th: Merge remote-tracking branch 'glitch/main' (d033fab0ed)

th-downstream
Kouhai 1 year ago
commit 7c7355a964

@ -1,3 +0,0 @@
---
ignore:
- CVE-2015-9284 # Mitigation following https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284#mitigating-in-rails-applications

@ -1,31 +1,29 @@
// For more details, see https://aka.ms/devcontainer.json.
{
"name": "Mastodon",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/sshd:1": {}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
"runServices": ["app", "db", "redis"],
"forwardPorts": [3000, 4000],
// Use 'postCreateCommand' to run commands after the container is created.
"containerEnv": {
"ES_ENABLED": "",
"LIBRE_TRANSLATE_ENDPOINT": ""
},
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"postCreateCommand": ".devcontainer/post-create.sh",
"waitFor": "postCreateCommand",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
}
}

@ -2,3 +2,7 @@ VAGRANT=true
LOCAL_DOMAIN=mastodon.local
BIND=0.0.0.0
DB_HOST=/var/run/postgresql/
ES_ENABLED=true
ES_HOST=localhost
ES_PORT=9200

@ -1,20 +1,21 @@
{
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
extends: [
'config:base',
':dependencyDashboard',
'config:recommended',
':labels(dependencies)',
':maintainLockFilesMonthly', // update non-direct dependencies monthly
':prConcurrentLimit10', // only 10 open PRs at the same time
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
],
stabilityDays: 3, // Wait 3 days after the package has been published before upgrading it
minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it
// packageRules order is important, they are applied from top to bottom and are merged,
// meaning the most important ones must be at the bottom, for example grouping rules
// If we do not want a package to be grouped with others, we need to set its groupName
// to `null` after any other rule set it to something.
dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).',
packageRules: [
{
// Ignore major version bumps for these node packages
// Require Dependency Dashboard Approval for major version bumps of these node packages
matchManagers: ['npm'],
matchPackageNames: [
'tesseract.js', // Requires code changes
@ -41,10 +42,10 @@
'react-router-dom',
],
matchUpdateTypes: ['major'],
enabled: false,
dependencyDashboardApproval: true,
},
{
// Ignore major version bumps for these Ruby packages
// Require Dependency Dashboard Approval for major version bumps of these Ruby packages
matchManagers: ['bundler'],
matchPackageNames: [
'rack', // Needs to be synced with Rails version
@ -55,7 +56,7 @@
'redis', // Requires manual upgrade and sync with Sidekiq version
],
matchUpdateTypes: ['major'],
enabled: false,
dependencyDashboardApproval: true,
},
{
// Update Github Actions and Docker images weekly
@ -63,25 +64,25 @@
extends: ['schedule:weekly'],
},
{
// Ignore major & minor bumps for the ruby image, this needs to be synced with .ruby-version
// Require Dependency Dashboard Approval for major & minor bumps for the ruby image, this needs to be synced with .ruby-version
matchManagers: ['dockerfile'],
matchPackageNames: ['moritzheiber/ruby-jemalloc'],
matchUpdateTypes: ['minor', 'major'],
enabled: false,
dependencyDashboardApproval: true,
},
{
// Ignore major bump for the node image, this needs to be synced with .nvmrc
// Require Dependency Dashboard Approval for major bumps for the node image, this needs to be synced with .nvmrc
matchManagers: ['dockerfile'],
matchPackageNames: ['node'],
matchUpdateTypes: ['major'],
enabled: false,
dependencyDashboardApproval: true,
},
{
// Ignore major postgres bumps in the docker-compose file, as those break dev environments
// Require Dependency Dashboard Approval for major postgres bumps in the docker-compose file, as those break dev environments
matchManagers: ['docker-compose'],
matchPackageNames: ['postgres'],
matchUpdateTypes: ['major'],
enabled: false,
dependencyDashboardApproval: true,
},
{
// Update devDependencies every week, with one grouped PR

@ -16,7 +16,7 @@ jobs:
env:
TZ: Etc/UTC
run: |
echo mastodon_version_suffix=nightly-$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
echo mastodon_version_suffix=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
outputs:
suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
@ -28,8 +28,8 @@ jobs:
use_native_arm64_builder: false
push_to_images: |
ghcr.io/${{ github.repository_owner }}/mastodon
# The `+` is important here, result will be v4.1.2+nightly-2022-03-05
version_suffix: +${{ needs.compute-suffix.outputs.suffix }}
# The `-` is important here, result will be v4.1.2-nightly.2022-03-05
version_suffix: -${{ needs.compute-suffix.outputs.suffix }}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: |

@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.54.2.
# using RuboCop version 1.56.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@ -61,38 +61,8 @@ Lint/EmptyBlock:
- 'spec/fabricators/access_token_fabricator.rb'
- 'spec/fabricators/conversation_fabricator.rb'
- 'spec/fabricators/system_key_fabricator.rb'
- 'spec/helpers/admin/action_logs_helper_spec.rb'
- 'spec/lib/activitypub/adapter_spec.rb'
- 'spec/models/account_alias_spec.rb'
- 'spec/models/account_deletion_request_spec.rb'
- 'spec/models/account_moderation_note_spec.rb'
- 'spec/models/announcement_mute_spec.rb'
- 'spec/models/announcement_reaction_spec.rb'
- 'spec/models/announcement_spec.rb'
- 'spec/models/backup_spec.rb'
- 'spec/models/conversation_mute_spec.rb'
- 'spec/models/custom_filter_keyword_spec.rb'
- 'spec/models/custom_filter_spec.rb'
- 'spec/models/device_spec.rb'
- 'spec/models/encrypted_message_spec.rb'
- 'spec/models/featured_tag_spec.rb'
- 'spec/models/follow_recommendation_suppression_spec.rb'
- 'spec/models/list_account_spec.rb'
- 'spec/models/list_spec.rb'
- 'spec/models/login_activity_spec.rb'
- 'spec/models/mute_spec.rb'
- 'spec/models/preview_card_spec.rb'
- 'spec/models/preview_card_trend_spec.rb'
- 'spec/models/relay_spec.rb'
- 'spec/models/scheduled_status_spec.rb'
- 'spec/models/status_stat_spec.rb'
- 'spec/models/status_trend_spec.rb'
- 'spec/models/system_key_spec.rb'
- 'spec/models/tag_follow_spec.rb'
- 'spec/models/unavailable_domain_spec.rb'
- 'spec/models/user_invite_request_spec.rb'
- 'spec/models/user_role_spec.rb'
- 'spec/models/web/setting_spec.rb'
Lint/NonLocalExitFromIterator:
Exclude:
@ -135,7 +105,7 @@ Lint/UselessAssignment:
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 143
Max: 144
Exclude:
- 'app/serializers/initial_state_serializer.rb'
@ -166,6 +136,19 @@ Naming/VariableNumber:
- 'spec/models/domain_block_spec.rb'
- 'spec/models/user_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeMultiline.
Performance/DeletePrefix:
Exclude:
- 'app/models/featured_tag.rb'
Performance/MapMethodChain:
Exclude:
- 'app/models/feed.rb'
- 'lib/mastodon/cli/maintenance.rb'
- 'spec/services/bulk_import_service_spec.rb'
- 'spec/services/import_service_spec.rb'
RSpec/AnyInstance:
Exclude:
- 'spec/controllers/activitypub/inboxes_controller_spec.rb'
@ -765,6 +748,15 @@ Style/RedundantFetchBlock:
- 'config/initializers/paperclip.rb'
- 'config/puma.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowMultipleReturnValues.
Style/RedundantReturn:
Exclude:
- 'app/controllers/api/v1/directories_controller.rb'
- 'app/controllers/auth/confirmations_controller.rb'
- 'app/lib/ostatus/tag_manager.rb'
- 'app/models/form/import.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
# AllowedMethods: present?, blank?, presence, try, try!

@ -2,6 +2,259 @@
All notable changes to this project will be documented in this file.
## [4.2.0] - UNRELEASED
The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by [@danielmbrasil](https://github.com/danielmbrasil), [@mjankowski](https://github.com/mjankowski), [@nschonni](https://github.com/nschonni), [@renchap](https://github.com/renchap), and [@takayamaki](https://github.com/takayamaki).
### Added
- **Add “Privacy and reach” tab in profile settings** ([Gargron](https://github.com/mastodon/mastodon/pull/26484), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26508))
This reorganized scattered privacy and reach settings to a single place, as well as improve their wording.
- **Add display of out-of-band hashtags in the web interface** ([Gargron](https://github.com/mastodon/mastodon/pull/26492), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26497), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26506), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26525))
- **Add role badges to the web interface** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25649), [Gargron](https://github.com/mastodon/mastodon/pull/26281))
- **Add ability to pick domains to forward reports to using the `forward_to_domains` parameter in `POST /api/v1/reports`** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25866))
The `forward_to_domains` REST API parameter is a list of strings. If it is empty or omitted, the previous behavior is maintained.
The `forward` parameter still needs to be set for `forward_to_domains` to be taken into account.
The forwarded-to domains can only include that of the original author and people being replied to.
- **Add forwarding of reported replies to servers being replied to** ([Gargron](https://github.com/mastodon/mastodon/pull/25341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26189))
- Add direct link to the Single-Sign On provider if there is only one sign up method available ([CSDUMMI](https://github.com/mastodon/mastodon/pull/26083), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26368))
- **Add webhook templating** ([Gargron](https://github.com/mastodon/mastodon/pull/23289))
- **Add webhooks for local `status.created`, `status.updated`, `account.updated` and `report.updated`** ([VyrCossont](https://github.com/mastodon/mastodon/pull/24133), [VyrCossont](https://github.com/mastodon/mastodon/pull/24243), [VyrCossont](https://github.com/mastodon/mastodon/pull/24211))
- **Add exclusive lists** ([dariusk](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324))
- **Add a confirmation screen when suspending a domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25144), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25603))
- **Add support for importing lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25203), [mgmn](https://github.com/mastodon/mastodon/pull/26120), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26372))
- **Add optional hCaptcha support** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25019), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25057), [Gargron](https://github.com/mastodon/mastodon/pull/25395), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26388))
- **Add lines to threads in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24549), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24677), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24696), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24711), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24713), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24715), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24800), [teeerevor](https://github.com/mastodon/mastodon/pull/25706), [renchap](https://github.com/mastodon/mastodon/pull/25807))
- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561))
- **Add `S3_DISABLE_CHECKSUM_MODE` environment variable for compatibility with some S3-compatible providers** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435))
- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510))
- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448))
- Add support for `indexable` attribute on remote actors ([Gargron](https://github.com/mastodon/mastodon/pull/26485))
- Add `DELETE /api/v1/profile/avatar` and `DELETE /api/v1/profile/header` to the REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25124), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26573))
- Add `ES_PRESET` option to customize numbers of shards and replicas ([Gargron](https://github.com/mastodon/mastodon/pull/26483), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26489))
This can have a value of `single_node_cluster` (default), `small_cluster` (uses one replica) or `large_cluster` (uses one replica and a higher number of shards).
- Add missing `instances` option to `tootctl search deploy` ([tribela](https://github.com/mastodon/mastodon/pull/26461))
- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542))
- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295))
- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443))
- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447))
- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300))
- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155))
- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149))
- Add stricter protocol fields validation for accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25937))
- Add support for Azure blob storage ([mistydemeo](https://github.com/mastodon/mastodon/pull/23607), [mistydemeo](https://github.com/mastodon/mastodon/pull/26080))
- Add toast with option to open post after publishing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25564), [Signez](https://github.com/mastodon/mastodon/pull/25919))
- Add canonical link tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25715))
- Add button to see results for polls in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25726))
- Add at-symbol prepended to mention span title ([forsamori](https://github.com/mastodon/mastodon/pull/25684))
- Add users index on `unconfirmed_email` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25672), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25702))
- Add superapp index on `oauth_applications` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25670))
- Add index to backups on `user_id` column ([mjankowski](https://github.com/mastodon/mastodon/pull/25647))
- Add onboarding prompt when home feed too slow in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25267), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25556), [Gargron](https://github.com/mastodon/mastodon/pull/25579), [renchap](https://github.com/mastodon/mastodon/pull/25580), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25581), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25617), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25917))
- Add `POST /api/v1/conversations/:id/unread` API endpoint to mark a conversation as unread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25509))
- Add `translate="no"` to outgoing mentions and links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25524))
- Add unsubscribe link and headers to e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/25378), [c960657](https://github.com/mastodon/mastodon/pull/26085))
- Add logging of websocket send errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25280))
- Add time zone preference ([Gargron](https://github.com/mastodon/mastodon/pull/25342), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26025))
- Add `legal` as report category ([Gargron](https://github.com/mastodon/mastodon/pull/23941), [renchap](https://github.com/mastodon/mastodon/pull/25400), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26509))
- Add `data-nosnippet` so Google doesn't use trending posts in snippets for `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25279))
- Add card with who invited you to join when displaying rules on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23475))
- Add missing primary keys to `accounts_tags` and `statuses_tags` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25210))
- Add support for custom sign-up URLs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25014), [renchap](https://github.com/mastodon/mastodon/pull/25108), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25190), [mgmn](https://github.com/mastodon/mastodon/pull/25531))
This is set using `SSO_ACCOUNT_SIGN_UP` and reflected in the REST API by adding `registrations.sign_up_url` to the `/api/v2/instance` endpoint.
- Add polling and automatic redirection to `/start` on email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25013))
- Add ability to block sign-ups from IP using the CLI ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24870))
- Add ALT badges to media that has alternative text in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24782), [c960657](https://github.com/mastodon/mastodon/pull/26166)
- Add ability to include accounts with pending follow requests in lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19727), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24810))
- Add trend management to admin API ([rrgeorge](https://github.com/mastodon/mastodon/pull/24257))
- `POST /api/v1/admin/trends/statuses/:id/approve`
- `POST /api/v1/admin/trends/statuses/:id/reject`
- `POST /api/v1/admin/trends/links/:id/approve`
- `POST /api/v1/admin/trends/links/:id/reject`
- `POST /api/v1/admin/trends/tags/:id/approve`
- `POST /api/v1/admin/trends/tags/:id/reject`
- `GET /api/v1/admin/trends/links/publishers`
- `POST /api/v1/admin/trends/links/publishers/:id/approve`
- `POST /api/v1/admin/trends/links/publishers/:id/reject`
- Add user handle to notification mail recipient address ([HeitorMC](https://github.com/mastodon/mastodon/pull/24240))
- Add progress indicator to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/24545))
- Add client-side validation for taken username in sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24546))
- Add `--approve` option to `tootctl accounts create` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24533))
- Add “In Memoriam” banner back to profiles ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23591), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23614))
This adds the `memorial` attribute to the `Account` REST API entity.
- Add colour to follow button when hashtag is being followed ([c960657](https://github.com/mastodon/mastodon/pull/24361))
- Add further explanations to the profile link verification instructions ([drzax](https://github.com/mastodon/mastodon/pull/19723))
- Add a link to Identity provider's account settings from the account settings ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24628))
- Add support for streaming server to connect to postgres with self-signed certs through the `sslmode` URL parameter ([ramuuns](https://github.com/mastodon/mastodon/pull/21431))
- Add support for specifying S3 storage classes through the `S3_STORAGE_CLASS` environment variable ([hyl](https://github.com/mastodon/mastodon/pull/22480))
- Add support for incoming rich text ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23913))
- Add support for Ruby 3.2 ([tenderlove](https://github.com/mastodon/mastodon/pull/22928), [casperisfine](https://github.com/mastodon/mastodon/pull/24142), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24202))
- Add API parameter to safeguard unexpected mentions in new posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18350))
### Changed
- **Change hashtags to be displayed separately when they are the last line of a post** ([renchap](https://github.com/mastodon/mastodon/pull/26499))
- **Change reblogs to be excluded from "Posts and replies" tab in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26302))
- **Change interaction modal in web interface** ([Gargron, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26075), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26268), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26267), [mgmn](https://github.com/mastodon/mastodon/pull/26459))
- **Change design of link previews in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26136), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26151), [Gargron](https://github.com/mastodon/mastodon/pull/26153), [Gargron](https://github.com/mastodon/mastodon/pull/26250), [Gargron](https://github.com/mastodon/mastodon/pull/26287), [Gargron](https://github.com/mastodon/mastodon/pull/26286), [c960657](https://github.com/mastodon/mastodon/pull/26184))
- **Change "direct message" nomenclature to "private mention" in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24248))
- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452))
- **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378))
- **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874))
- **Change local and federated timelines to be in a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247))
- **Change user archive export to be faster and more reliable, and export `.zip` archives instead of `.tar.gz` ones** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23360), [TheEssem](https://github.com/mastodon/mastodon/pull/25034))
- **Change `mastodon-streaming` systemd unit files to be templated** ([e-nomem](https://github.com/mastodon/mastodon/pull/24751))
- **Change `statsd` integration to disable sidekiq metrics by default** ([mjankowski](https://github.com/mastodon/mastodon/pull/25265), [mjankowski](https://github.com/mastodon/mastodon/pull/25336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26310))
This deprecates `statsd` support and disables the sidekiq integration unless `STATSD_SIDEKIQ` is set to `true`.
This is because the `nsa` gem is unmaintained, and its sidekiq integration is known to add very significant overhead.
Later versions of Mastodon will have other ways to get the same metrics.
- **Change replica support to native Rails adapter** ([krainboltgreene](https://github.com/mastodon/mastodon/pull/25693), [Gargron](https://github.com/mastodon/mastodon/pull/25849), [Gargron](https://github.com/mastodon/mastodon/pull/25874), [Gargron](https://github.com/mastodon/mastodon/pull/25851), [Gargron](https://github.com/mastodon/mastodon/pull/25977), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26074), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26386))
This is a breaking change, dropping `makara` support, and requiring you to update your database configuration if you are using replicas.
To tell Mastodon to use a read replica, you can either set the `REPLICA_DB_NAME` environment variable (along with `REPLICA_DB_USER`, `REPLICA_DB_PASS`, `REPLICA_DB_HOST`, and `REPLICA_DB_PORT`, if they differ from the primary database), or the `REPLICA_DATABASE_URL` environment variable if your configuration is based on `DATABASE_URL`.
- Change follow recommendation materialized view to be faster in most cases ([renchap, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26545))
- Change `robots.txt` to block GPTBot ([Foritus](https://github.com/mastodon/mastodon/pull/26396))
- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26416))
- Change streaming `/metrics` to include additional metrics ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26299))
- Change indexing frequency from 5 minutes to 1 minute, add locks to schedulers ([Gargron](https://github.com/mastodon/mastodon/pull/26304))
- Change column link to add a better keyboard focus indicator ([teeerevor](https://github.com/mastodon/mastodon/pull/26278))
- Change poll form element colors to fit with the rest of the ui ([teeerevor](https://github.com/mastodon/mastodon/pull/26139), [teeerevor](https://github.com/mastodon/mastodon/pull/26162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26164))
- Change 'favourite' to 'favorite' for American English ([marekr](https://github.com/mastodon/mastodon/pull/24667), [gunchleoc](https://github.com/mastodon/mastodon/pull/26009), [nabijaczleweli](https://github.com/mastodon/mastodon/pull/26109))
- Change ActivityStreams representation of suspended accounts to not use a blank `name` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25276))
- Change focus UI for keyboard only input ([teeerevor](https://github.com/mastodon/mastodon/pull/25935), [Gargron](https://github.com/mastodon/mastodon/pull/26125))
- Change thread view to scroll to the selected post rather than the post being replied to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24685))
- Change links in multi-column mode so tabs are open in single-column mode ([Signez](https://github.com/mastodon/mastodon/pull/25893), [Signez](https://github.com/mastodon/mastodon/pull/26070), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25973))
- Change searching with `#` to include account index ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25638))
- Change label and design of sensitive and unavailable media in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25712), [Gargron](https://github.com/mastodon/mastodon/pull/26135), [Gargron](https://github.com/mastodon/mastodon/pull/26330))
- Change button colors to increase hover/focus contrast and consistency ([teeerevor](https://github.com/mastodon/mastodon/pull/25677), [Gargron](https://github.com/mastodon/mastodon/pull/25679))
- Change dropdown icon above compose form from ellipsis to bars in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25661))
- Change header backgrounds to use fewer different colors in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25577))
- Change files to be deleted in batches instead of one-by-one ([Gargron](https://github.com/mastodon/mastodon/pull/23302), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25586), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25587))
- Change emoji picker icon ([iparr](https://github.com/mastodon/mastodon/pull/25479))
- Change edit profile page ([Gargron](https://github.com/mastodon/mastodon/pull/25413), [c960657](https://github.com/mastodon/mastodon/pull/26538))
- Change "bot" label to "automated" ([Gargron](https://github.com/mastodon/mastodon/pull/25356))
- Change design of dropdowns in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25107))
- Change wording of “Content cache retention period” setting to highlight destructive implications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23261))
- Change autolinking to allow carets in URL search params ([renchap](https://github.com/mastodon/mastodon/pull/25216))
- Change share action from being in action bar to being in dropdown in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25105))
- Change remote report processing to accept reports with long comments, but truncate them ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25028))
- Change sessions to be ordered from most-recent to least-recently updated ([frankieroberto](https://github.com/mastodon/mastodon/pull/25005))
- Change vacuum scheduler to also delete expired tokens and unused application records ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24871))
- Change "Sign in" to "Login" ([Gargron](https://github.com/mastodon/mastodon/pull/24942))
- Change domain suspensions to also be checked before trying to fetch unknown remote resources ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24535))
- Change media components to use aspect-ratio rather than compute height themselves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24686), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24943))
- Change logo version in header based on screen size in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24707))
- Change label from "For you" to "People" on explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24706))
- Change logged-out WebUI HTML pages to be cached for a few seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24708))
- Change unauthenticated responses to be cached in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/24348), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24665))
- Change HTTP caching logic ([Gargron](https://github.com/mastodon/mastodon/pull/24347), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24604))
- Change hashtags and mentions in bios to open in-app in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24643))
- Change styling of the recommended accounts to allow bio to be more visible ([chike00](https://github.com/mastodon/mastodon/pull/24480))
- Change account search in moderation interface to allow searching by username including the leading `@` ([HeitorMC](https://github.com/mastodon/mastodon/pull/24242))
- Change all components to use the same error page in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24512))
- Change search pop-out in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24305))
- Change user settings to be stored in a more optimal way ([Gargron](https://github.com/mastodon/mastodon/pull/23630), [c960657](https://github.com/mastodon/mastodon/pull/24321), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24558), [Gargron](https://github.com/mastodon/mastodon/pull/24761), [Gargron](https://github.com/mastodon/mastodon/pull/24783), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25508), [jsgoldstein](https://github.com/mastodon/mastodon/pull/25340))
- Change media upload limits and remove client-side resizing ([Gargron](https://github.com/mastodon/mastodon/pull/23726))
- Change design of account rows in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24247), [Gargron](https://github.com/mastodon/mastodon/pull/24343), [Gargron](https://github.com/mastodon/mastodon/pull/24956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25131))
- Change log-out to use Single Logout when using external log-in through OIDC ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24020))
- Change sidekiq-bulk's batch size from 10,000 to 1,000 jobs in one Redis call ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24034))
- Change translation to only be offered for supported languages ([c960657](https://github.com/mastodon/mastodon/pull/23879), [c960657](https://github.com/mastodon/mastodon/pull/24037))
This adds the `/api/v1/instance/translation_languages` REST API endpoint that returns an object with the supported translation language pairs in the form:
```json
{
"fr": ["en", "de"]
}
```
(where `fr` is a supported source language and `en` and `de` or supported output language when translating a `fr` string)
- Change compose form checkbox to native input with `appearance: none` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22949))
- Change posts' clickable area to be larger ([c960657](https://github.com/mastodon/mastodon/pull/23621))
- Change `followed_by` link to `location=all` if account is local on /admin/accounts/:id page ([tribela](https://github.com/mastodon/mastodon/pull/23467))
### Removed
- **Remove support for Node.js 14** ([renchap](https://github.com/mastodon/mastodon/pull/25198))
- **Remove support for Ruby 2.7** ([nschonni](https://github.com/mastodon/mastodon/pull/24237))
- **Remove clustering from streaming API** ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24655))
- **Remove anonymous access to the streaming API** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23989))
- Remove 16:9 cropping from web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26132))
- Remove back button from bookmarks, favourites and lists screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26126))
- Remove display name input from sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24704))
- Remove `tai` locale ([c960657](https://github.com/mastodon/mastodon/pull/23880))
- Remove empty Kushubian (csb) local files ([nschonni](https://github.com/mastodon/mastodon/pull/24151))
- Remove `Permissions-Policy` header from all responses ([Gargron](https://github.com/mastodon/mastodon/pull/24124))
### Fixed
- **Fix filters not being applying in the explore page** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25887))
- **Fix being unable to load past a full page of filtered posts in Home timeline** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24930))
- **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073))
- **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218))
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
- Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375))
- Fix light theme select option for hashtags ([teeerevor](https://github.com/mastodon/mastodon/pull/26311))
- Fix AVIF attachments ([c960657](https://github.com/mastodon/mastodon/pull/26264))
- Fix incorrect URL normalization when fetching remote resources ([c960657](https://github.com/mastodon/mastodon/pull/26219), [c960657](https://github.com/mastodon/mastodon/pull/26285))
- Fix being unable to filter posts for individual Chinese languages ([gunchleoc](https://github.com/mastodon/mastodon/pull/26066))
- Fix preview card sometimes linking to 4xx error pages ([c960657](https://github.com/mastodon/mastodon/pull/26200))
- Fix emoji picker button scrolling with textarea content in single-column view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25304))
- Fix missing border on error screen in light theme in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26152))
- Fix UI overlap with the loupe icon in the Explore Tab ([gol-cha](https://github.com/mastodon/mastodon/pull/26113))
- Fix unexpected redirection to `/explore` after sign-in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26143))
- Fix `/api/v1/statuses/:id/unfavourite` and `/api/v1/statuses/:id/unreblog` returning non-updated counts ([c960657](https://github.com/mastodon/mastodon/pull/24365))
- Fix clicking the “Back” button sometimes leading out of Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953), [CSFlorin](https://github.com/mastodon/mastodon/pull/24835), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/24867), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25281))
- Fix processing of `null` ActivityPub activities ([tribela](https://github.com/mastodon/mastodon/pull/26021))
- Fix hashtag posts not being removed from home feed on hashtag unfollow ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26028))
- Fix for "follows you" indicator in light web UI not readable ([vmstan](https://github.com/mastodon/mastodon/pull/25993))
- Fix incorrect line break between icon and number of reposts & favourites ([edent](https://github.com/mastodon/mastodon/pull/26004))
- Fix sounds not being loaded from assets host ([Signez](https://github.com/mastodon/mastodon/pull/25931))
- Fix buttons showing inconsistent styles ([teeerevor](https://github.com/mastodon/mastodon/pull/25903), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25965), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26482))
- Fix trend calculation working on too many items at a time ([Gargron](https://github.com/mastodon/mastodon/pull/25835))
- Fix dropdowns being disabled for logged out users in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25964))
- Fix explore page being inaccessible when opted-out of trends in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25716))
- Fix re-activated accounts possibly getting deleted by `AccountDeletionWorker` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25711))
- Fix `/api/v2/search` not working with following query param ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25681))
- Fix inefficient query when requesting a new confirmation email from a logged-in account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25669))
- Fix unnecessary concurrent calls to `/api/*/instance` in web UI ([mgmn](https://github.com/mastodon/mastodon/pull/25663))
- Fix resolving local URL for remote content ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637))
- Fix search not being easily findable on smaller screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25576), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25631))
- Fix j/k keyboard shortcuts on some status lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25554))
- Fix missing validation on `default_privacy` setting ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25513))
- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477))
- Fix non-interactive upload container being given a `button` role and tabIndex ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25462))
- Fix always redirecting to onboarding in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25396))
- Fix inconsistent use of middle dot (·) instead of bullet (•) to separate items ([j-f1](https://github.com/mastodon/mastodon/pull/25248))
- Fix spacing of middle dots in the detailed status meta section ([j-f1](https://github.com/mastodon/mastodon/pull/25247))
- Fix prev/next buttons color in media viewer ([renchap](https://github.com/mastodon/mastodon/pull/25231))
- Fix email addresses not being properly updated in `tootctl maintenance fix-duplicates` ([mjankowski](https://github.com/mastodon/mastodon/pull/25118))
- Fix unicode surrogate pairs sometimes being broken in page title ([eai04191](https://github.com/mastodon/mastodon/pull/25148))
- Fix various inefficient queries against account domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25126))
- Fix video player offering to expand in a lightbox when it's in an `iframe` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25067))
- Fix post embed previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25071))
- Fix inadequate error handling in several API controllers when given invalid parameters ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24947), [danielmbrasil](https://github.com/mastodon/mastodon/pull/24958), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25063), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25072), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25386), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25595))
- Fix uncaught `ActiveRecord::StatementInvalid` in Mastodon::IpBlocksCLI ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24861))
- Fix various edge cases with local moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24812))
- Fix `tootctl accounts cull` crashing when encountering a domain resolving to a private address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23378))
- Fix `tootctl accounts approve --number N` not aproving the N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605))
- Fix being unable to clear media description when editing posts ([c960657](https://github.com/mastodon/mastodon/pull/24720))
- Fix unavailable translations not falling back to English ([mgmn](https://github.com/mastodon/mastodon/pull/24727))
- Fix anonymous visitors getting a session cookie on first visit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24584), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24650), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24664))
- Fix cutting off first letter of hashtag links sometimes in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24623))
- Fix crash in `tootctl accounts create --reattach --force` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24557), [danielmbrasil](https://github.com/mastodon/mastodon/pull/24680))
- Fix characters being emojified even when using Variation Selector 15 (text) ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20949), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24615))
- Fix uncaught ActiveRecord::StatementInvalid exception in `Mastodon::AccountsCLI#approve` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24590))
- Fix email confirmation skip option in `tootctl accounts modify USERNAME --email EMAIL --confirm` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24578))
- Fix tooltip for dates without time ([c960657](https://github.com/mastodon/mastodon/pull/24244))
- Fix missing loading spinner and loading more on scroll in Private Mentions column ([c960657](https://github.com/mastodon/mastodon/pull/24446))
- Fix account header image missing from `/settings/profile` on narrow screens ([c960657](https://github.com/mastodon/mastodon/pull/24433))
- Fix height of announcements not being updated when using reduced animations ([c960657](https://github.com/mastodon/mastodon/pull/24354))
- Fix inconsistent radius in advanced interface drawer ([thislight](https://github.com/mastodon/mastodon/pull/24407))
- Fix loading more trending posts on scroll in the advanced interface ([OmmyZhang](https://github.com/mastodon/mastodon/pull/24314))
- Fix poll ending notification for edited polls ([c960657](https://github.com/mastodon/mastodon/pull/24311))
- Fix max width of media in `/about` and `/privacy-policy` ([mgmn](https://github.com/mastodon/mastodon/pull/24180))
- Fix streaming API not being usable without `DATABASE_URL` ([Gargron](https://github.com/mastodon/mastodon/pull/23960))
- Fix external authentication not running onboarding code for new users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23458))
## [4.1.6] - 2023-07-31
### Fixed

@ -35,11 +35,14 @@ group :pam_authentication, optional: true do
end
gem 'net-ldap', '~> 0.18'
gem 'omniauth-cas', '~> 2.0'
gem 'omniauth-saml', '~> 1.10'
# TODO: Point back at released omniauth-cas gem when PR merged
# https://github.com/dlindahl/omniauth-cas/pull/68
gem 'omniauth-cas', github: 'stanhu/omniauth-cas', ref: '4211e6d05941b4a981f9a36b49ec166cecd0e271'
gem 'omniauth-saml', '~> 2.0'
gem 'omniauth_openid_connect', '~> 0.6.1'
gem 'omniauth', '~> 1.9'
gem 'omniauth-rails_csrf_protection', '~> 0.1'
gem 'omniauth', '~> 2.0'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2'
@ -56,8 +59,9 @@ gem 'httplog', '~> 1.6.2'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15'
gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b'
gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14'
gem 'parslet'
@ -106,7 +110,7 @@ group :test do
gem 'fuubar', '~> 2.5'
# Extra RSpec extenion methods and helpers for sidekiq
gem 'rspec-sidekiq', '~> 3.1'
gem 'rspec-sidekiq', '~> 4.0'
# Browser integration testing
gem 'capybara', '~> 3.39'

@ -7,6 +7,17 @@ GIT
hkdf (~> 0.2)
jwt (~> 2.0)
GIT
remote: https://github.com/jhawthorn/nsa.git
revision: e020fcc3a54d993ab45b7194d89ab720296c111b
ref: e020fcc3a54d993ab45b7194d89ab720296c111b
specs:
nsa (0.2.8)
activesupport (>= 4.2, < 7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
GIT
remote: https://github.com/mastodon/rails-settings-cached.git
revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab
@ -15,50 +26,60 @@ GIT
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
GIT
remote: https://github.com/stanhu/omniauth-cas.git
revision: 4211e6d05941b4a981f9a36b49ec166cecd0e271
ref: 4211e6d05941b4a981f9a36b49ec166cecd0e271
specs:
omniauth-cas (2.0.0)
addressable (~> 2.3)
nokogiri (~> 1.5)
omniauth (>= 1.2, < 3)
GEM
remote: https://rubygems.org/
specs:
actioncable (7.0.6)
actionpack (= 7.0.6)
activesupport (= 7.0.6)
actioncable (7.0.7.2)
actionpack (= 7.0.7.2)
activesupport (= 7.0.7.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (7.0.6)
actionpack (= 7.0.6)
activejob (= 7.0.6)
activerecord (= 7.0.6)
activestorage (= 7.0.6)
activesupport (= 7.0.6)
actionmailbox (7.0.7.2)
actionpack (= 7.0.7.2)
activejob (= 7.0.7.2)
activerecord (= 7.0.7.2)
activestorage (= 7.0.7.2)
activesupport (= 7.0.7.2)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.0.6)
actionpack (= 7.0.6)
actionview (= 7.0.6)
activejob (= 7.0.6)
activesupport (= 7.0.6)
actionmailer (7.0.7.2)
actionpack (= 7.0.7.2)
actionview (= 7.0.7.2)
activejob (= 7.0.7.2)
activesupport (= 7.0.7.2)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (7.0.6)
actionview (= 7.0.6)
activesupport (= 7.0.6)
actionpack (7.0.7.2)
actionview (= 7.0.7.2)
activesupport (= 7.0.7.2)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.6)
actionpack (= 7.0.6)
activerecord (= 7.0.6)
activestorage (= 7.0.6)
activesupport (= 7.0.6)
actiontext (7.0.7.2)
actionpack (= 7.0.7.2)
activerecord (= 7.0.7.2)
activestorage (= 7.0.7.2)
activesupport (= 7.0.7.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.0.6)
activesupport (= 7.0.6)
actionview (7.0.7.2)
activesupport (= 7.0.7.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -68,22 +89,22 @@ GEM
activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.0.6)
activesupport (= 7.0.6)
activejob (7.0.7.2)
activesupport (= 7.0.7.2)
globalid (>= 0.3.6)
activemodel (7.0.6)
activesupport (= 7.0.6)
activerecord (7.0.6)
activemodel (= 7.0.6)
activesupport (= 7.0.6)
activestorage (7.0.6)
actionpack (= 7.0.6)
activejob (= 7.0.6)
activerecord (= 7.0.6)
activesupport (= 7.0.6)
activemodel (7.0.7.2)
activesupport (= 7.0.7.2)
activerecord (7.0.7.2)
activemodel (= 7.0.7.2)
activesupport (= 7.0.7.2)
activestorage (7.0.7.2)
actionpack (= 7.0.7.2)
activejob (= 7.0.7.2)
activerecord (= 7.0.7.2)
activesupport (= 7.0.7.2)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (7.0.6)
activesupport (7.0.7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@ -103,8 +124,8 @@ GEM
attr_required (1.0.1)
awrence (1.2.1)
aws-eventstream (1.2.0)
aws-partitions (1.791.0)
aws-sdk-core (3.178.0)
aws-partitions (1.793.0)
aws-sdk-core (3.180.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5)
@ -112,8 +133,8 @@ GEM
aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.131.0)
aws-sdk-core (~> 3, >= 3.177.0)
aws-sdk-s3 (1.132.1)
aws-sdk-core (~> 3, >= 3.179.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0)
@ -126,6 +147,7 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8)
base64 (0.1.1)
bcrypt (3.1.18)
better_errors (2.10.1)
erubi (>= 1.0.0)
@ -248,7 +270,7 @@ GEM
tzinfo
excon (0.100.0)
fabrication (2.30.0)
faker (3.2.0)
faker (3.2.1)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday-em_http (~> 1.0)
@ -312,7 +334,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.49.2)
haml_lint (0.49.3)
haml (>= 4.0, < 6.2)
parallel (~> 1.10)
rainbow
@ -408,7 +430,7 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.12.0)
lograge (0.13.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
@ -431,12 +453,12 @@ GEM
hashie (~> 5.0)
memory_profiler (1.0.1)
method_source (1.0.0)
mime-types (3.4.1)
mime-types (3.5.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_mime (1.1.2)
mini_portile2 (2.8.2)
minitest (5.18.1)
mime-types-data (3.2023.0808)
mini_mime (1.1.5)
mini_portile2 (2.8.4)
minitest (5.19.0)
msgpack (1.7.1)
multi_json (1.15.0)
multipart-post (2.3.0)
@ -444,7 +466,7 @@ GEM
uri
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.3.6)
net-imap (0.3.7)
date
net-protocol
net-ldap (0.18.0)
@ -458,23 +480,20 @@ GEM
net-protocol
net-ssh (7.1.0)
nio4r (2.5.9)
nokogiri (1.15.3)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.15.0)
omniauth (1.9.2)
oj (3.16.0)
omniauth (2.1.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-cas (2.0.0)
addressable (~> 2.3)
nokogiri (~> 1.5)
omniauth (~> 1.2)
omniauth-rails_csrf_protection (0.1.2)
rack (>= 2.2.3)
rack-protection
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (>= 1.3.1)
omniauth-saml (1.10.3)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.9)
omniauth (~> 2.0)
omniauth-saml (2.1.0)
omniauth (~> 2.0)
ruby-saml (~> 1.12)
omniauth_openid_connect (0.6.1)
omniauth (>= 1.9, < 3)
openid_connect (~> 1.1)
@ -515,15 +534,15 @@ GEM
premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0)
public_suffix (5.0.3)
puma (6.3.0)
puma (6.3.1)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.1)
rack (2.2.7)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack (2.2.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-oauth2 (1.21.3)
@ -532,30 +551,33 @@ GEM
httpclient
json-jwt (>= 1.11.0)
rack (>= 2.1.0)
rack-protection (3.0.5)
rack
rack-proxy (0.7.6)
rack
rack-test (2.1.0)
rack (>= 1.3)
rails (7.0.6)
actioncable (= 7.0.6)
actionmailbox (= 7.0.6)
actionmailer (= 7.0.6)
actionpack (= 7.0.6)
actiontext (= 7.0.6)
actionview (= 7.0.6)
activejob (= 7.0.6)
activemodel (= 7.0.6)
activerecord (= 7.0.6)
activestorage (= 7.0.6)
activesupport (= 7.0.6)
rails (7.0.7.2)
actioncable (= 7.0.7.2)
actionmailbox (= 7.0.7.2)
actionmailer (= 7.0.7.2)
actionpack (= 7.0.7.2)
actiontext (= 7.0.7.2)
actionview (= 7.0.7.2)
activejob (= 7.0.7.2)
activemodel (= 7.0.7.2)
activerecord (= 7.0.7.2)
activestorage (= 7.0.7.2)
activesupport (= 7.0.7.2)
bundler (>= 1.15.0)
railties (= 7.0.6)
railties (= 7.0.7.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
rails-dom-testing (2.1.1)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
@ -563,9 +585,9 @@ GEM
rails-i18n (7.0.7)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
railties (7.0.6)
actionpack (= 7.0.6)
activesupport (= 7.0.6)
railties (7.0.7.2)
actionpack (= 7.0.7.2)
activesupport (= 7.0.7.2)
method_source
rake (>= 12.2)
thor (~> 1.0)
@ -612,12 +634,15 @@ GEM
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.12.0)
rspec-sidekiq (4.0.1)
rspec-core (~> 3.0)
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8)
rspec-support (3.12.1)
rspec_chunked (0.6)
rubocop (1.54.2)
rubocop (1.56.1)
base64 (~> 0.1.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
@ -625,7 +650,7 @@ GEM
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
@ -634,14 +659,14 @@ GEM
rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-performance (1.18.0)
rubocop-performance (1.19.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.22.0)
rubocop-rspec (2.23.2)
rubocop (~> 1.33)
rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
@ -662,7 +687,7 @@ GEM
scenic (1.7.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
selenium-webdriver (4.9.1)
selenium-webdriver (4.11.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@ -706,6 +731,7 @@ GEM
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.25)
statsd-ruby (1.5.0)
stoplight (3.0.1)
redlock (~> 1.0)
strong_migrations (0.8.0)
@ -720,7 +746,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
test-prof (1.2.1)
test-prof (1.2.2)
thor (1.2.2)
tilt (2.2.0)
timeout (0.4.0)
@ -780,14 +806,14 @@ GEM
railties (>= 5.2)
semantic_range (>= 2.3.0)
websocket (1.2.9)
websocket-driver (0.7.5)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wisper (2.0.1)
xorcist (1.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.8)
zeitwerk (2.6.11)
PLATFORMS
ruby
@ -854,15 +880,16 @@ DEPENDENCIES
mario-redis-lock (~> 1.2)
md-paperclip-azure (~> 2.2)
memory_profiler
mime-types (~> 3.4.1)
mime-types (~> 3.5.0)
net-http (~> 0.3.2)
net-ldap (~> 0.18)
nokogiri (~> 1.15)
nsa!
oj (~> 3.14)
omniauth (~> 1.9)
omniauth-cas (~> 2.0)
omniauth-rails_csrf_protection (~> 0.1)
omniauth-saml (~> 1.10)
omniauth (~> 2.0)
omniauth-cas!
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1)
ox (~> 2.14)
parslet
@ -888,7 +915,7 @@ DEPENDENCIES
redis-namespace (~> 1.10)
rqrcode (~> 2.2)
rspec-rails (~> 6.0)
rspec-sidekiq (~> 3.1)
rspec-sidekiq (~> 4.0)
rspec_chunked (~> 0.6)
rubocop
rubocop-capybara

@ -1,8 +1,11 @@
# Security Policy
If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can reach us at <security@joinmastodon.org>.
If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can either:
You should _not_ report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk.
- open a [Github security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new)
- reach us at <security@joinmastodon.org>
You should _not_ report such issues on public GitHub issues or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk.
## Scope

43
Vagrantfile vendored

@ -60,6 +60,37 @@ sudo usermod -a -G rvm $USER
SCRIPT
$provisionElasticsearch = <<SCRIPT
# Install Elastic Search
sudo apt install openjdk-17-jre-headless -y
sudo wget -O /usr/share/keyrings/elasticsearch.asc https://artifacts.elastic.co/GPG-KEY-elasticsearch
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/elasticsearch.asc] https://artifacts.elastic.co/packages/7.x/apt stable main" > /etc/apt/sources.list.d/elastic-7.x.list'
sudo apt update
sudo apt install elasticsearch -y
sudo systemctl daemon-reload
sudo systemctl enable --now elasticsearch
echo 'path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 0.0.0.0
http.port: 9200
discovery.seed_hosts: ["localhost"]
cluster.initial_master_nodes: ["node-1"]' > /etc/elasticsearch/elasticsearch.yml
sudo systemctl restart elasticsearch
# Install Kibana
sudo apt install kibana -y
sudo systemctl enable --now kibana
echo 'server.host: "0.0.0.0"
elasticsearch.hosts: ["http://localhost:9200"]' > /etc/kibana/kibana.yml
sudo systemctl restart kibana
SCRIPT
$provisionB = <<SCRIPT
source "/etc/profile.d/rvm.sh"
@ -102,10 +133,8 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb|
vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "4096"]
# Increase the number of CPUs. Uncomment and adjust to
# increase performance
# vb.customize ["modifyvm", :id, "--cpus", "3"]
vb.customize ["modifyvm", :id, "--memory", "8192"]
vb.customize ["modifyvm", :id, "--cpus", "3"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172
@ -141,9 +170,15 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.network :forwarded_port, guest: 3000, host: 3000
config.vm.network :forwarded_port, guest: 4000, host: 4000
config.vm.network :forwarded_port, guest: 8080, host: 8080
config.vm.network :forwarded_port, guest: 9200, host: 9200
config.vm.network :forwarded_port, guest: 9300, host: 9300
config.vm.network :forwarded_port, guest: 9243, host: 9243
config.vm.network :forwarded_port, guest: 5601, host: 5601
# Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision'
config.vm.provision :shell, inline: $provisionA, privileged: false, reset: true
# Run with elevated privileges for Elasticsearch installation
config.vm.provision :shell, inline: $provisionElasticsearch, privileged: true
config.vm.provision :shell, inline: $provisionB, privileged: false
config.vm.post_up_message = <<MESSAGE

@ -1,7 +1,7 @@
# frozen_string_literal: true
class AccountsIndex < Chewy::Index
settings index: { refresh_interval: '30s' }, analysis: {
settings index: index_preset(refresh_interval: '30s'), analysis: {
filter: {
english_stop: {
type: 'stop',
@ -33,7 +33,7 @@ class AccountsIndex < Chewy::Index
},
verbatim: {
tokenizer: 'whitespace',
tokenizer: 'standard',
filter: %w(lowercase asciifolding cjk_width),
},

@ -1,12 +1,12 @@
# frozen_string_literal: true
class InstancesIndex < Chewy::Index
settings index: { refresh_interval: '30s' }
settings index: index_preset(refresh_interval: '30s')
index_scope ::Instance.searchable
root date_detection: false do
field :domain, type: 'text', index_prefixes: { min_chars: 1 }
field :domain, type: 'text', index_prefixes: { min_chars: 1, max_chars: 5 }
field :accounts_count, type: 'long'
end
end

@ -3,7 +3,7 @@
class StatusesIndex < Chewy::Index
include FormattingHelper
settings index: { refresh_interval: '30s' }, analysis: {
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
filter: {
english_stop: {
type: 'stop',

@ -1,7 +1,7 @@
# frozen_string_literal: true
class TagsIndex < Chewy::Index
settings index: { refresh_interval: '30s' }, analysis: {
settings index: index_preset(refresh_interval: '30s'), analysis: {
analyzer: {
content: {
tokenizer: 'keyword',

@ -40,7 +40,7 @@ module Admin
end
# Allow transparently upgrading a domain block
if existing_domain_block.present?
if existing_domain_block.present? && existing_domain_block.domain == TagManager.instance.normalize_domain(@domain_block.domain.strip)
@domain_block = existing_domain_block
@domain_block.assign_attributes(resource_params)
end

@ -0,0 +1,21 @@
# frozen_string_literal: true
class Api::V1::Instances::LanguagesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale
before_action :set_languages
vary_by ''
def show
cache_even_if_authenticated!
render json: @languages, each_serializer: REST::LanguageSerializer
end
private
def set_languages
@languages = LanguagesHelper::SUPPORTED_LOCALES.keys.map { |code| LanguagePresenter.new(code) }
end
end

@ -0,0 +1,13 @@
# frozen_string_literal: true
class Api::V1::Profile::AvatarsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end

@ -0,0 +1,13 @@
# frozen_string_literal: true
class Api::V1::Profile::HeadersController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user!
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end

@ -42,7 +42,7 @@ module CaptchaConcern
end
def extend_csp_for_captcha!
policy = request.content_security_policy
policy = request.content_security_policy&.clone
return unless captcha_required? && policy.present?
@ -54,6 +54,8 @@ module CaptchaConcern
policy.send(directive, *values)
end
request.content_security_policy = policy
end
def render_captcha

@ -12,7 +12,7 @@ module WebAppControllerConcern
end
def skip_csrf_meta_tags?
current_user.nil?
!(ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
end
def set_app_body_class

@ -0,0 +1,27 @@
# frozen_string_literal: true
class Settings::PrivacyController < Settings::BaseController
before_action :set_account
def show; end
def update
if UpdateAccountService.new.call(@account, account_params.except(:settings))
current_user.update!(settings_attributes: account_params[:settings])
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show
end
end
private
def account_params
params.require(:account).permit(:discoverable, :unlocked, :show_collections, settings: UserSettings.keys)
end
def set_account
@account = current_account
end
end

@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController
private
def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, :hide_collections, fields_attributes: [:name, :value])
params.require(:account).permit(:display_name, :note, :avatar, :header, :bot, fields_attributes: [:name, :value])
end
def set_account

@ -21,6 +21,8 @@ module ContextHelper
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' },
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: {
'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId',

@ -188,6 +188,7 @@ module LanguagesHelper
ISO_639_3 = {
ast: ['Asturian', 'Asturianu'].freeze,
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
cnr: ['Montenegrin', 'crnogorski'].freeze,
jbo: ['Lojban', 'la .lojban.'].freeze,
@ -200,6 +201,7 @@ module LanguagesHelper
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
szl: ['Silesian', 'ślůnsko godka'].freeze,
tok: ['Toki Pona', 'toki pona'].freeze,
xal: ['Kalmyk', 'Хальмг келн'].freeze,
zba: ['Balaibalan', 'باليبلن'].freeze,
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
}.freeze

@ -18,22 +18,14 @@ delegate(document, '#account_display_name', 'input', ({ target }) => {
}
});
delegate(document, '#account_avatar', 'change', ({ target }) => {
const avatar = document.querySelector('.card .avatar img');
delegate(document, '#edit_profile input[type=file]', 'change', ({ target }) => {
const avatar = document.getElementById(target.id + '-preview');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
avatar.src = url;
});
delegate(document, '#account_header', 'change', ({ target }) => {
const header = document.querySelector('.card .card__img img');
const [file] = target.files || [];
const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
header.src = url;
});
delegate(document, '#account_locked', 'change', ({ target }) => {
const lock = document.querySelector('.card .display-name i');

@ -77,7 +77,10 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.quote = normalOldStatus.get('quote');
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
normalStatus.translation = normalOldStatus.get('translation');
if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation');
}
} else {
const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');

@ -157,7 +157,7 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId });
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

@ -8,6 +8,7 @@ import classNames from 'classnames';
import api from 'flavours/glitch/api';
const messages = defineMessages({
legal: { id: 'report.categories.legal', defaultMessage: 'Legal' },
other: { id: 'report.categories.other', defaultMessage: 'Other' },
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
@ -150,6 +151,7 @@ class ReportReasonSelector extends PureComponent {
return (
<div className='report-reason-selector'>
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === 'legal'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}

@ -18,7 +18,19 @@ export default class Column extends PureComponent {
};
scrollTop () {
const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
let scrollable = null;
if (this.props.bindToDocument) {
scrollable = document.scrollingElement;
} else {
scrollable = this.node.querySelector('.scrollable');
// Some columns have nested `.scrollable` containers, with the outer one
// being a wrapper while the actual scrollable content is deeper.
if (scrollable.classList.contains('scrollable--flex')) {
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
}
}
if (!scrollable) {
return;

@ -196,9 +196,9 @@ class Header extends ImmutablePureComponent {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames({ 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
actionBtn = <Button className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}

@ -23,6 +23,7 @@ export default class Story extends PureComponent {
author: PropTypes.string,
sharedTimes: PropTypes.number,
thumbnail: PropTypes.string,
thumbnailDescription: PropTypes.string,
blurhash: PropTypes.string,
expanded: PropTypes.bool,
};
@ -34,7 +35,7 @@ export default class Story extends PureComponent {
handleImageLoad = () => this.setState({ thumbnailLoaded: true });
render () {
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, blurhash } = this.props;
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props;
const { thumbnailLoaded } = this.state;
@ -50,7 +51,7 @@ export default class Story extends PureComponent {
{thumbnail ? (
<>
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
<img src={thumbnail} onLoad={this.handleImageLoad} alt='' role='presentation' />
<img src={thumbnail} onLoad={this.handleImageLoad} alt={thumbnailDescription} title={thumbnailDescription} lang={lang} />
</>
) : <Skeleton />}
</div>

@ -67,6 +67,7 @@ class Links extends PureComponent {
author={link.get('author_name')}
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
thumbnail={link.get('image')}
thumbnailDescription={link.get('image_description')}
blurhash={link.get('blurhash')}
/>
))}

@ -110,10 +110,10 @@ class Results extends PureComponent {
return (
<>
<div className='account__section-headline'>
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
<button onClick={this.handleSelectAll} className={type === 'all' ? 'active' : undefined}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' ? 'active' : undefined}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' ? 'active' : undefined}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
</div>
<div className='explore__search-results'>

@ -0,0 +1,79 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Button from 'flavours/glitch/components/button';
import { ShortNumber } from 'flavours/glitch/components/short_number';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
});
const usesRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const peopleRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const usesTodayRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses_today'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
if (!tag) {
return null;
}
const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]);
const dividingCircle = <span aria-hidden>{' · '}</span>;
return (
<div className='hashtag-header'>
<div className='hashtag-header__header'>
<h1>#{tag.get('name')}</h1>
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
</div>
<div>
<ShortNumber value={uses} renderer={usesRenderer} />
{dividingCircle}
<ShortNumber value={people} renderer={peopleRenderer} />
{dividingCircle}
<ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} />
</div>
</div>
);
});
HashtagHeader.propTypes = {
tag: ImmutablePropTypes.map,
disabled: PropTypes.bool,
onClick: PropTypes.func,
intl: PropTypes.object,
};

@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes';
@ -17,17 +16,11 @@ import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/ac
import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header';
import { Icon } from 'flavours/glitch/components/icon';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import { HashtagHeader } from './components/hashtag_header';
import ColumnSettingsContainer from './containers/column_settings_container';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
});
const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
tag: state.getIn(['tags', props.params.id]),
@ -48,7 +41,6 @@ class HashtagTimeline extends PureComponent {
hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
intl: PropTypes.object,
};
handlePin = () => {
@ -188,27 +180,11 @@ class HashtagTimeline extends PureComponent {
};
render () {
const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
const { hasUnread, columnId, multiColumn, tag } = this.props;
const { id, local } = this.props.params;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
let followButton;
if (tag) {
const following = tag.get('following');
const classes = classNames('column-header__button', {
active: following,
});
followButton = (
<button className={classes} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
</button>
);
}
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
<ColumnHeader
@ -220,13 +196,14 @@ class HashtagTimeline extends PureComponent {
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
extraButton={followButton}
showBackButton
>
{columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader>
<StatusListContainer
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}${local ? ':local' : ''}`}
@ -245,4 +222,4 @@ class HashtagTimeline extends PureComponent {
}
export default connect(mapStateToProps)(injectIntl(HashtagTimeline));
export default connect(mapStateToProps)(HashtagTimeline);

@ -13,7 +13,7 @@ import { openModal, closeModal } from 'flavours/glitch/actions/modal';
import api from 'flavours/glitch/api';
import Button from 'flavours/glitch/components/button';
import { Icon } from 'flavours/glitch/components/icon';
import { registrationsOpen } from 'flavours/glitch/initial_state';
import { registrationsOpen, sso_redirect } from 'flavours/glitch/initial_state';
const messages = defineMessages({
loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' },
@ -21,12 +21,16 @@ const messages = defineMessages({
const mapStateToProps = (state, { accountId }) => ({
displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
});
const mapDispatchToProps = (dispatch) => ({
onSignupClick() {
dispatch(closeModal());
dispatch(openModal('CLOSED_REGISTRATIONS'));
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
},
});
@ -294,6 +298,7 @@ class InteractionModal extends React.PureComponent {
url: PropTypes.string,
type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']),
onSignupClick: PropTypes.func.isRequired,
signupUrl: PropTypes.string.isRequired,
};
handleSignupClick = () => {
@ -301,7 +306,7 @@ class InteractionModal extends React.PureComponent {
};
render () {
const { url, type, displayNameHtml } = this.props;
const { url, type, displayNameHtml, signupUrl } = this.props;
const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />;
@ -332,9 +337,15 @@ class InteractionModal extends React.PureComponent {
let signupButton;
if (registrationsOpen) {
if (sso_redirect) {
signupButton = (
<a href='/auth/sign_up' className='link-button'>
<a href={sso_redirect} data-method='post' className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);

@ -178,7 +178,8 @@ export default class Card extends PureComponent {
dummy={!useBlurhash}
/>
);
let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
const thumbnailDescription = card.get('image_description');
const thumbnail = <img src={card.get('image')} alt={thumbnailDescription} title={thumbnailDescription} lang={language} style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'>

@ -611,7 +611,7 @@ class Status extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 && list.get(i - 1)}
previousId={i > 0 ? list.get(i - 1) : undefined}
nextId={list.get(i + 1) || (ancestors && statusId)}
rootId={statusId}
/>

@ -1,761 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { HotKeys } from 'react-hotkeys';
import { initBlockModal } from 'flavours/glitch/actions/blocks';
import { initBoostModal } from 'flavours/glitch/actions/boosts';
import {
replyCompose,
mentionCompose,
directCompose,
} from 'flavours/glitch/actions/compose';
import {
favourite,
unfavourite,
bookmark,
unbookmark,
reblog,
unreblog,
pin,
unpin,
} from 'flavours/glitch/actions/interactions';
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
import { openModal } from 'flavours/glitch/actions/modal';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initReport } from 'flavours/glitch/actions/reports';
import {
fetchStatus,
muteStatus,
unmuteStatus,
deleteStatus,
editStatus,
hideStatus,
revealStatus,
translateStatus,
undoStatusTranslation,
} from 'flavours/glitch/actions/statuses';
import { Icon } from 'flavours/glitch/components/icon';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import { textForScreenReader, defaultMediaVisibility } from 'flavours/glitch/components/status';
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import StatusContainer from 'flavours/glitch/containers/status_container';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import Column from 'flavours/glitch/features/ui/components/column';
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state';
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
import ColumnHeader from '../../components/column_header';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import ActionBar from './components/action_bar';
import DetailedStatus from './components/detailed_status';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
tootHeading: { id: 'account.posts_with_replies', defaultMessage: 'Posts and replies' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.List();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id && !mutable.includes(id)) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
});
const getDescendantsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
state => state.get('statuses'),
], (statusId, contextReplies, statuses) => {
let descendantsIds = [];
const ids = [statusId];
while (ids.length > 0) {
let id = ids.pop();
const replies = contextReplies.get(id);
if (statusId !== id) {
descendantsIds.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
});
}
}
let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
if (insertAt !== -1) {
descendantsIds.forEach((id, idx) => {
if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
descendantsIds.splice(idx, 1);
descendantsIds.splice(insertAt, 0, id);
insertAt += 1;
}
});
}
return Immutable.List(descendantsIds);
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List();
if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
}
return {
isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']),
status,
ancestorsIds,
descendantsIds,
settings: state.get('local_settings'),
askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
};
};
return mapStateToProps;
};
const truncate = (str, num) => {
const arr = Array.from(str);
if (arr.length > num) {
return arr.slice(0, num).join('') + '…';
} else {
return str;
}
};
const titleFromStatus = (intl, status) => {
const displayName = status.getIn(['account', 'display_name']);
const username = status.getIn(['account', 'username']);
const user = displayName.trim().length === 0 ? username : displayName;
const text = status.get('search_index');
const attachmentCount = status.get('media_attachments').size;
return text ? `${user}: "${truncate(text, 30)}"` : intl.formatMessage(messages.statusTitleWithAttachments, { user, attachmentCount });
};
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
identity: PropTypes.object,
};
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
status: ImmutablePropTypes.map,
isLoading: PropTypes.bool,
settings: ImmutablePropTypes.map.isRequired,
ancestorsIds: ImmutablePropTypes.list.isRequired,
descendantsIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
askReplyConfirmation: PropTypes.bool,
multiColumn: PropTypes.bool,
domain: PropTypes.string.isRequired,
pictureInPicture: ImmutablePropTypes.contains({
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
};
state = {
fullscreen: false,
isExpanded: undefined,
threadExpanded: undefined,
statusId: undefined,
loadedStatusId: undefined,
showMedia: undefined,
revealBehindCW: undefined,
};
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
this.props.dispatch(fetchStatus(this.props.params.statusId));
}
static getDerivedStateFromProps(props, state) {
let update = {};
let updated = false;
if (props.params.statusId && state.statusId !== props.params.statusId) {
props.dispatch(fetchStatus(props.params.statusId));
update.threadExpanded = undefined;
update.statusId = props.params.statusId;
updated = true;
}
const revealBehindCW = props.settings.getIn(['media', 'reveal_behind_cw']);
if (revealBehindCW !== state.revealBehindCW) {
update.revealBehindCW = revealBehindCW;
if (revealBehindCW) update.showMedia = defaultMediaVisibility(props.status, props.settings);
updated = true;
}
if (props.status && state.loadedStatusId !== props.status.get('id')) {
update.showMedia = defaultMediaVisibility(props.status, props.settings);
update.loadedStatusId = props.status.get('id');
update.isExpanded = autoUnfoldCW(props.settings, props.status);
updated = true;
}
return updated ? update : null;
}
handleToggleHidden = () => {
const { status } = this.props;
if (this.props.settings.getIn(['content_warnings', 'shared_state'])) {
if (status.get('hidden')) {
this.props.dispatch(revealStatus(status.get('id')));
} else {
this.props.dispatch(hideStatus(status.get('id')));
}
} else if (this.props.status.get('spoiler_text')) {
this.setExpansion(!this.state.isExpanded);
}
};
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
};
handleModalFavourite = (status) => {
this.props.dispatch(favourite(status));
};
handleFavouriteClick = (status, e) => {
const { dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
if ((e && e.shiftKey) || !favouriteModal) {
this.handleModalFavourite(status);
} else {
dispatch(openModal({
modalType: 'FAVOURITE',
modalProps: {
status,
onFavourite: this.handleModalFavourite,
},
}));
}
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'favourite',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
handlePin = (status) => {
if (status.get('pinned')) {
this.props.dispatch(unpin(status));
} else {
this.props.dispatch(pin(status));
}
};
handleReplyClick = (status) => {
const { askReplyConfirmation, dispatch, intl } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (askReplyConfirmation) {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
},
}));
} else {
dispatch(replyCompose(status, this.context.router.history));
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reply',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
handleModalReblog = (status, privacy) => {
const { dispatch } = this.props;
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status, privacy));
}
};
handleReblogClick = (status, e) => {
const { settings, dispatch } = this.props;
const { signedIn } = this.context.identity;
if (signedIn) {
if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
} else if ((e && e.shiftKey) || !boostModal) {
this.handleModalReblog(status);
} else {
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
}
} else {
dispatch(openModal({
modalType: 'INTERACTION',
modalProps: {
type: 'reblog',
accountId: status.getIn(['account', 'id']),
url: status.get('url'),
},
}));
}
};
handleBookmarkClick = (status) => {
if (status.get('bookmarked')) {
this.props.dispatch(unbookmark(status));
} else {
this.props.dispatch(bookmark(status));
}
};
handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props;
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal({
modalType: 'CONFIRM',
modalProps: {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
},
}));
}
};
handleEditClick = (status, history) => {
this.props.dispatch(editStatus(status.get('id'), history));
};
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
};
handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router));
};
handleOpenMedia = (media, index, lang) => {
this.props.dispatch(openModal({
modalType: 'MEDIA',
modalProps: { statusId: this.props.status.get('id'), media, index, lang },
}));
};
handleOpenVideo = (media, lang, options) => {
this.props.dispatch(openModal({
modalType: 'VIDEO',
modalProps: { statusId: this.props.status.get('id'), media, lang, options },
}));
};
handleHotkeyOpenMedia = e => {
const { status } = this.props;
e.preventDefault();
if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
};
handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
};
handleConversationMuteClick = (status) => {
if (status.get('muted')) {
this.props.dispatch(unmuteStatus(status.get('id')));
} else {
this.props.dispatch(muteStatus(status.get('id')));
}
};
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds, settings } = this.props;
const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
let { isExpanded } = this.state;
if (settings.getIn(['content_warnings', 'shared_state']))
isExpanded = !status.get('hidden');
if (!isExpanded) {
this.props.dispatch(revealStatus(statusIds));
} else {
this.props.dispatch(hideStatus(statusIds));
}
this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded });
};
handleTranslate = status => {
const { dispatch } = this.props;
if (status.get('translation')) {
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
} else {
dispatch(translateStatus(status.get('id')));
}
};
handleBlockClick = (status) => {
const { dispatch } = this.props;
const account = status.get('account');
dispatch(initBlockModal(account));
};
handleReport = (status) => {
this.props.dispatch(initReport(status.get('account'), status));
};
handleEmbed = (status) => {
this.props.dispatch(openModal({
modalType: 'EMBED',
modalProps: { id: status.get('id') },
}));
};
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
};
handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id'));
};
handleHotkeyMoveDown = () => {
this.handleMoveDown(this.props.status.get('id'));
};
handleHotkeyReply = e => {
e.preventDefault();
this.handleReplyClick(this.props.status);
};
handleHotkeyFavourite = () => {
this.handleFavouriteClick(this.props.status);
};
handleHotkeyBoost = () => {
this.handleReblogClick(this.props.status);
};
handleHotkeyBookmark = () => {
this.handleBookmarkClick(this.props.status);
};
handleHotkeyMention = e => {
e.preventDefault();
this.handleMentionClick(this.props.status);
};
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
};
handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size - 1, true);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index, true);
} else {
this._selectChild(index - 1, true);
}
}
};
handleMoveDown = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
if (id === status.get('id')) {
this._selectChild(ancestorsIds.size + 1, false);
} else {
let index = ancestorsIds.indexOf(id);
if (index === -1) {
index = descendantsIds.indexOf(id);
this._selectChild(ancestorsIds.size + index + 2, false);
} else {
this._selectChild(index + 1, false);
}
}
};
_selectChild (index, align_top) {
const container = this.node;
const element = container.querySelectorAll('.focusable')[index];
if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus();
}
}
handleHeaderClick = () => {
this.column.scrollTop();
};
renderChildren (list, ancestors) {
const { params: { statusId } } = this.props;
return list.map((id, i) => (
<StatusContainer
key={id}
id={id}
expanded={this.state.threadExpanded}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 && list.get(i - 1)}
nextId={list.get(i + 1) || (ancestors && statusId)}
rootId={statusId}
/>
));
}
setExpansion = value => {
this.setState({ isExpanded: value });
};
setRef = c => {
this.node = c;
};
setColumnRef = c => {
this.column = c;
};
componentDidUpdate (prevProps) {
const { status, ancestorsIds, multiColumn } = this.props;
if (status && (ancestorsIds.size > prevProps.ancestorsIds.size || prevProps.status?.get('id') !== status.get('id'))) {
window.requestAnimationFrame(() => {
this.node?.querySelector('.detailed-status__wrapper')?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that.
if (!multiColumn) {
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
if (offset) {
const scrollingElement = document.scrollingElement || document.body;
scrollingElement.scrollBy(0, -offset);
}
}
});
}
}
componentWillUnmount () {
detachFullscreenListener(this.onFullScreenChange);
}
onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
};
render () {
let ancestors, descendants;
const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
if (isLoading) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
if (status === null) {
return (
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
}
if (descendantsIds && descendantsIds.size > 0) {
descendants = <>{this.renderChildren(descendantsIds)}</>;
}
const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1;
const isIndexable = !status.getIn(['account', 'noindex']);
const handlers = {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
bookmark: this.handleHotkeyBookmark,
mention: this.handleHotkeyMention,
openProfile: this.handleHotkeyOpenProfile,
toggleSpoiler: this.handleToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};
return (
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.detailedStatus)}>
<ColumnHeader
icon='comment'
title={intl.formatMessage(messages.tootHeading)}
onClick={this.handleHeaderClick}
showBackButton
multiColumn={multiColumn}
extraButton={(
<button type='button' className='column-header__button' title={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(!isExpanded ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll}><Icon id={!isExpanded ? 'eye-slash' : 'eye'} /></button>
)}
/>
<ScrollContainer scrollKey='thread'>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
{ancestors}
<HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper', `detailed-status__wrapper-${status.get('visibility')}`)} tabIndex={0} aria-label={textForScreenReader(intl, status, false, isExpanded)}>
<DetailedStatus
key={`details-${status.get('id')}`}
status={status}
settings={settings}
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
expanded={isExpanded}
onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate}
domain={domain}
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
/>
<ActionBar
key={`action-bar-${status.get('id')}`}
status={status}
onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}
onMute={this.handleMuteClick}
onMuteConversation={this.handleConversationMuteClick}
onBlock={this.handleBlockClick}
onReport={this.handleReport}
onPin={this.handlePin}
onEmbed={this.handleEmbed}
/>
</div>
</HotKeys>
{descendants}
</div>
</ScrollContainer>
<Helmet>
<title>{titleFromStatus(intl, status)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={status.get('url')} />
</Helmet>
</Column>
);
}
}
export default injectIntl(connect(makeMapStateToProps)(Status));

@ -423,4 +423,4 @@ class FocalPointModal extends ImmutablePureComponent {
export default connect(mapStateToProps, mapDispatchToProps, null, {
forwardRef: true,
})(injectIntl(FocalPointModal, { withRef: true }));
})(injectIntl(FocalPointModal, { forwardRef: true }));

@ -13,7 +13,7 @@ import { Avatar } from 'flavours/glitch/components/avatar';
import { Icon } from 'flavours/glitch/components/icon';
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
import Permalink from 'flavours/glitch/components/permalink';
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
import { registrationsOpen, me, sso_redirect } from 'flavours/glitch/initial_state';
const Account = connect(state => ({
account: state.getIn(['accounts', me]),
@ -74,28 +74,35 @@ class Header extends PureComponent {
</>
);
} else {
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
if (sso_redirect) {
content = (
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
)
} else {
signupButton = (
<button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
}
content = (
<>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</>
);
}
content = (
<>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</>
);
}
return (

@ -105,14 +105,7 @@ export default class ModalRoot extends PureComponent {
handleClose = (ignoreFocus = false) => {
const { onClose } = this.props;
let message = null;
try {
message = this._modal?.getWrappedInstance?.().getCloseConfirmationMessage?.();
} catch (_) {
// injectIntl defines `getWrappedInstance` but errors out if `withRef`
// isn't set.
// This would be much smoother with react-intl 3+ and `forwardRef`.
}
const message = this._modal?.getCloseConfirmationMessage?.();
onClose(message, ignoreFocus);
};
@ -133,7 +126,10 @@ export default class ModalRoot extends PureComponent {
{visible && (
<>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
{(SpecificComponent) => {
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />
}}
</BundleContainer>
<Helmet>

@ -63,7 +63,7 @@ class ReportModal extends ImmutablePureComponent {
dispatch(submitReport({
account_id: accountId,
status_ids: selectedStatusIds.toArray(),
selected_domains: selectedDomains.toArray(),
forward_to_domains: selectedDomains.toArray(),
comment,
forward: selectedDomains.size > 0,
category,

@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { openModal } from 'flavours/glitch/actions/modal';
import { registrationsOpen } from 'flavours/glitch/initial_state';
import { registrationsOpen, sso_redirect } from 'flavours/glitch/initial_state';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
const SignInBanner = () => {
@ -18,6 +18,15 @@ const SignInBanner = () => {
const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up');
if (sso_redirect) {
return (
<div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
</div>
)
}
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button button--block'>

@ -82,6 +82,7 @@
* @property {boolean} use_blurhash
* @property {boolean=} use_pending_items
* @property {string} version
* @property {string} sso_redirect
* @property {boolean} translation_enabled
* @property {string} status_page_url
* @property {boolean} system_emoji_font
@ -160,6 +161,7 @@ export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const languages = initialState?.languages;
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
// Glitch-soc-specific settings
export const maxChars = (initialState && initialState.max_toot_chars) || 500;

@ -1,10 +1,43 @@
{
"compose.attach": "Vedhæft...",
"compose.attach.doodle": "Tegn noget",
"compose.attach.upload": "Upload en fil",
"compose_form.poll.multiple_choices": "Tillad flere valg",
"confirmations.missing_media_description.message": "Mindst én vedhæftet medie mangler en beskrivelse. Overvej at tilføje en beskrivelse af alle vedhæftede medier af hensyn til personer med nedsat syn, før du publicerer dit indlæg.",
"empty_column.follow_recommendations": "Det ser ud til, at der ikke kunne genereres forslag til dig. Du kan prøve med Søg for at lede efter personer, du måske kender, eller udforske hashtags.",
"follow_recommendations.done": "Udført",
"follow_recommendations.heading": "Følg personer du gerne vil se indlæg fra! Her er nogle forslag.",
"follow_recommendations.lead": "Indlæg, fra personer du følger, vil fremgå kronologisk ordnet i dit hjemmefeed. Vær ikke bange for at begå fejl, da du altid og meget nemt kan ændre dit valg!",
"home.column_settings.advanced": "Avanceret",
"home.column_settings.show_direct": "Vis private omtaler",
"navigation_bar.app_settings": "Appindstillinger",
"navigation_bar.misc": "Diverse",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",
"settings.preferences": "Preferences"
"settings.always_show_spoilers_field": "Vis altid feltet til indholdsadvarsel",
"settings.auto_collapse_media": "Indlæg med medier",
"settings.close": "Luk",
"settings.collapsed_statuses": "Sammenfoldede indlæg",
"settings.content_warnings": "Indholdsadvarsler",
"settings.content_warnings.regexp": "Regulært udtryk",
"settings.general": "Generelt",
"settings.image_backgrounds_media_hint": "Hvis et indlæg har vedhæftede medier, brug den første som baggrund",
"settings.media": "Medier",
"settings.preferences": "Præferencer",
"settings.rewrite_mentions": "Omskriv omtaler i viste indlæg",
"settings.rewrite_mentions_acct": "Omskriv med brugernavn og domæne (når brugeren ikke er lokal)",
"settings.rewrite_mentions_no": "Omskriv ikke omtaler",
"settings.rewrite_mentions_username": "Omskriv med brugernavn",
"settings.show_reply_counter": "Vis et estimat over antal svar",
"settings.status_icons": "Statusikoner",
"settings.status_icons_language": "Sprogindikator",
"settings.status_icons_local_only": "Kun lokal-indikator",
"settings.status_icons_media": "Medie- og afstemningsindikator",
"settings.status_icons_reply": "Svarindikator",
"settings.status_icons_visibility": "Statussynlighedsindikator",
"settings.tag_misleading_links": "Marker vildledende links",
"status.has_audio": "Har vedhæftede lydfiler",
"status.has_pictures": "Har vedhæftede billeder",
"status.has_preview_card": "Har en vedhæftet linkvisning",
"status.has_video": "Har vedhæftede videoer"
}

@ -1,8 +1,12 @@
{
"about.fork_disclaimer": "Glitch-socはMastodonからフォークされたフリーなオープンソースソフトウェアです。",
"account.add_account_note": "@{name}のメモを追加",
"account.disclaimer_full": "このユーザー情報は不正確な可能性があります。",
"account.follows": "フォロー",
"account.joined": "{date} に登録",
"account.mute_notifications": "@{name}さんからの通知を受け取らない",
"account.suspended_disclaimer_full": "このユーザーはモデレータにより停止されました。",
"account.unmute_notifications": "@{name}さんからの通知を受け取る",
"account.view_full_profile": "正確な情報を見る",
"account_note.cancel": "キャンセル",
"account_note.edit": "編集",
@ -16,20 +20,25 @@
"advanced_options.threaded_mode.short": "スレッドモード",
"advanced_options.threaded_mode.tooltip": "スレッドモードを有効にする",
"boost_modal.missing_description": "このトゥートには少なくとも1つの画像に説明が付与されていません",
"column.favourited_by": "お気に入りしたユーザー",
"column.heading": "その他",
"column.reblogged_by": "ブーストしたユーザー",
"column.subheading": "その他のオプション",
"column_header.profile": "プロフィール",
"column_subheading.lists": "リスト",
"column_subheading.navigation": "ナビゲーション",
"community.column_settings.allow_local_only": "ローカル限定投稿を表示する",
"compose.attach": "添付...",
"compose.attach.doodle": "お絵描きをする",
"compose.attach.upload": "ファイルをアップロード",
"compose.content-type.html": "HTML",
"compose.content-type.markdown": "マークダウン",
"compose.content-type.plain": "プレーンテキスト",
"compose_form.poll.multiple_choices": "複数回答を許可",
"compose_form.poll.single_choice": "単一回答を許可",
"compose_form.spoiler": "本文は警告の後ろに隠す",
"confirmation_modal.do_not_ask_again": "もう1度尋ねない",
"confirmations.deprecated_settings.confirm": "Mastodonの設定を使用",
"confirmations.missing_media_description.confirm": "このまま投稿",
"confirmations.missing_media_description.edit": "メディアを編集",
"confirmations.missing_media_description.message": "少なくとも1つの画像に視覚障害者のための画像説明が付与されていません。すべての画像に対して説明を付与することを望みます。",
@ -38,6 +47,7 @@
"confirmations.unfilter.edit_filter": "フィルターを編集",
"confirmations.unfilter.filters": "適用されたフィルター",
"content-type.change": "コンテンツ形式を変更",
"direct.group_by_conversations": "会話でグループ化",
"empty_column.follow_recommendations": "おすすめを生成できませんでした。検索を使って知り合いを探したり、トレンドハッシュタグを見てみましょう。",
"endorsed_accounts_editor.endorsed_accounts": "紹介しているユーザー",
"favourite_modal.combo": "次からは {combo} を押せば、これをスキップできます。",
@ -48,18 +58,22 @@
"home.column_settings.advanced": "高度",
"home.column_settings.filter_regex": "正規表現でフィルター",
"home.column_settings.show_direct": "DMを表示",
"home.settings": "カラムの設定",
"keyboard_shortcuts.bookmark": "ブックマーク",
"keyboard_shortcuts.secondary_toot": "セカンダリートゥートの公開範囲でトゥートする",
"keyboard_shortcuts.toggle_collapse": "折りたたむ/折りたたみを解除",
"media_gallery.sensitive": "閲覧注意",
"moved_to_warning": "このアカウント{moved_to_link}に引っ越したため、新しいフォロワーを受け入れていません。",
"navigation_bar.app_settings": "アプリ設定",
"navigation_bar.featured_users": "紹介しているアカウント",
"navigation_bar.keyboard_shortcuts": "キーボードショートカット",
"navigation_bar.misc": "その他",
"notification.markForDeletion": "選択",
"notification_purge.btn_all": "すべて\n選択",
"notification_purge.btn_apply": "選択したものを\n削除",
"notification_purge.btn_invert": "選択を\n反転",
"notification_purge.btn_none": "選択\n解除",
"notification_purge.start": "通知整理モードに入る",
"notifications.marked_clear": "選択した通知を削除する",
"notifications.marked_clear_confirmation": "削除した全ての通知を完全に削除してもよろしいですか?",
"onboarding.page_one.federation": "{domain}はMastodonのインスタンスです。Mastodonとは、独立したサーバが連携して作るソーシャルネットワークです。これらのサーバーをインスタンスと呼びます。",
@ -68,6 +82,7 @@
"settings.always_show_spoilers_field": "常にコンテンツワーニング設定を表示する(指定がない場合は通常投稿)",
"settings.auto_collapse": "自動折りたたみ",
"settings.auto_collapse_all": "すべて",
"settings.auto_collapse_height": "トゥートが長いと見なされる高さ(ピクセル)",
"settings.auto_collapse_lengthy": "長いトゥート",
"settings.auto_collapse_media": "メディア付きトゥート",
"settings.auto_collapse_notifications": "通知",
@ -82,6 +97,9 @@
"settings.content_warnings": "コンテンツワーニング",
"settings.content_warnings.regexp": "正規表現",
"settings.content_warnings_filter": "説明に指定した文字が含まれているものを自動で展開しないようにする",
"settings.content_warnings_media_outside": "コンテンツワーニングの外側にメディア添付ファイルを表示する",
"settings.content_warnings_shared_state": "すべてのコピーの内容を一度に表示/非表示",
"settings.content_warnings_unfold_opts": "自動展開オプション",
"settings.enable_collapsed": "トゥート折りたたみを有効にする",
"settings.enable_content_warnings_auto_unfold": "コンテンツワーニング指定されている投稿を常に表示する",
"settings.general": "一般",
@ -119,10 +137,24 @@
"settings.side_arm_reply_mode.copy": "返信先の投稿範囲を利用する",
"settings.side_arm_reply_mode.keep": "セカンダリートゥートボタンの設定を維持する",
"settings.side_arm_reply_mode.restrict": "返信先の投稿範囲に制限する",
"settings.status_icons": "トゥートアイコン",
"settings.status_icons_language": "言語インジケータ",
"settings.status_icons_local_only": "ローカル限定インジケータ",
"settings.status_icons_media": "メディア・アンケートインジケータ",
"settings.status_icons_reply": "返信インジケータ",
"settings.status_icons_visibility": "公開範囲インジケータ",
"settings.swipe_to_change_columns": "スワイプでカラムを切り替え可能にする(モバイルのみ)",
"settings.tag_misleading_links": "誤解を招くリンクにタグをつける",
"settings.tag_misleading_links.hint": "明示的に言及していないすべてのリンクに、リンクターゲットホストを含む視覚的な表示を追加します",
"settings.wide_view": "ワイドビュー(デスクトップ レイアウトのみ)",
"status.collapse": "折りたたむ",
"status.has_audio": "添付されたオーディオファイルが表示されます",
"status.has_pictures": "添付された画像が表示されます",
"status.has_preview_card": "添付されたプレビューカードが表示されます",
"status.has_video": "添付動画が表示されます",
"status.in_reply_to": "このトゥートは返信です",
"status.is_poll": "このトゥートはアンケートです",
"status.local_only": "あなたのインスタンスのみに公開",
"status.sensitive_toggle": "クリックして表示",
"status.uncollapse": "折りたたみを解除"
}

@ -14,7 +14,6 @@ import emojify from 'flavours/glitch/features/emoji/emoji';
import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions';
import { loadLocale, getLocale } from 'flavours/glitch/locales';
import { loadPolyfills } from 'flavours/glitch/polyfills';
import ready from 'flavours/glitch/ready';
const messages = defineMessages({
usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' },
@ -42,159 +41,157 @@ function main() {
};
};
ready(() => {
const locale = document.documentElement.lang;
const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
});
const dateFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
timeFormat: false,
});
const dateFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'short',
day: 'numeric',
timeFormat: false,
});
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
hour12: false,
});
const timeFormat = new Intl.DateTimeFormat(locale, {
timeStyle: 'short',
hour12: false,
});
const formatMessage = ({ id, defaultMessage }, values) => {
const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
return messageFormat.format(values);
};
const formatMessage = ({ id, defaultMessage }, values) => {
const messageFormat = new IntlMessageFormat(localeData[id] || defaultMessage, locale);
return messageFormat.format(values);
};
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
content.innerHTML = emojify(content.innerHTML);
});
[].forEach.call(document.querySelectorAll('.emojify'), (content) => {
content.innerHTML = emojify(content.innerHTML);
});
[].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const formattedDate = dateTimeFormat.format(datetime);
[].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const formattedDate = dateTimeFormat.format(datetime);
content.title = formattedDate;
content.textContent = formattedDate;
});
content.title = formattedDate;
content.textContent = formattedDate;
});
const isToday = date => {
const today = new Date();
const isToday = date => {
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
const todayFormat = new IntlMessageFormat(localeData['relative_format.today'] || 'Today at {time}', locale);
[].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
[].forEach.call(document.querySelectorAll('time.relative-formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
let formattedContent;
let formattedContent;
if (isToday(datetime)) {
const formattedTime = timeFormat.format(datetime);
if (isToday(datetime)) {
const formattedTime = timeFormat.format(datetime);
formattedContent = todayFormat.format({ time: formattedTime });
} else {
formattedContent = dateFormat.format(datetime);
}
formattedContent = todayFormat.format({ time: formattedTime });
} else {
formattedContent = dateFormat.format(datetime);
}
content.title = formattedContent;
content.textContent = formattedContent;
});
content.title = formattedContent;
content.textContent = formattedContent;
});
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const now = new Date();
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime'));
const now = new Date();
const timeGiven = content.getAttribute('datetime').includes('T');
content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
content.textContent = timeAgoString({
formatMessage,
formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
}, datetime, now, now.getFullYear(), timeGiven);
});
const timeGiven = content.getAttribute('datetime').includes('T');
content.title = timeGiven ? dateTimeFormat.format(datetime) : dateFormat.format(datetime);
content.textContent = timeAgoString({
formatMessage,
formatDate: (date, options) => (new Intl.DateTimeFormat(locale, options)).format(date),
}, datetime, now, now.getFullYear(), timeGiven);
});
const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) {
import(/* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container')
.then(({ default: MediaContainer }) => {
[].forEach.call(reactComponents, (component) => {
[].forEach.call(component.children, (child) => {
component.removeChild(child);
});
const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) {
import(/* webpackChunkName: "containers/media_container" */ 'flavours/glitch/containers/media_container')
.then(({ default: MediaContainer }) => {
[].forEach.call(reactComponents, (component) => {
[].forEach.call(component.children, (child) => {
component.removeChild(child);
});
const content = document.createElement('div');
const root = createRoot(content);
root.render(<MediaContainer locale={locale} components={reactComponents} />);
document.body.appendChild(content);
scrollToDetailedStatus();
})
.catch(error => {
console.error(error);
scrollToDetailedStatus();
});
const content = document.createElement('div');
const root = createRoot(content);
root.render(<MediaContainer locale={locale} components={reactComponents} />);
document.body.appendChild(content);
scrollToDetailedStatus();
})
.catch(error => {
console.error(error);
scrollToDetailedStatus();
});
} else {
scrollToDetailedStatus();
}
delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
const username = document.getElementById('user_account_attributes_username');
if (username.value && username.value.length > 0) {
axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
username.setCustomValidity(formatMessage(messages.usernameTaken));
}).catch(() => {
username.setCustomValidity('');
});
} else {
scrollToDetailedStatus();
username.setCustomValidity('');
}
}, 500, { leading: false, trailing: true }));
delegate(document, '#user_account_attributes_username', 'input', throttle(() => {
const username = document.getElementById('user_account_attributes_username');
delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
const password = document.getElementById('user_password');
const confirmation = document.getElementById('user_password_confirmation');
if (!confirmation) return;
if (username.value && username.value.length > 0) {
axios.get('/api/v1/accounts/lookup', { params: { acct: username.value } }).then(() => {
username.setCustomValidity(formatMessage(messages.usernameTaken));
}).catch(() => {
username.setCustomValidity('');
});
} else {
username.setCustomValidity('');
}
}, 500, { leading: false, trailing: true }));
delegate(document, '#user_password,#user_password_confirmation', 'input', () => {
const password = document.getElementById('user_password');
const confirmation = document.getElementById('user_password_confirmation');
if (!confirmation) return;
if (confirmation.value && confirmation.value.length > password.maxLength) {
confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
} else {
confirmation.setCustomValidity('');
}
});
if (confirmation.value && confirmation.value.length > password.maxLength) {
confirmation.setCustomValidity(formatMessage(messages.passwordExceedsLength));
} else if (password.value && password.value !== confirmation.value) {
confirmation.setCustomValidity(formatMessage(messages.passwordDoesNotMatch));
} else {
confirmation.setCustomValidity('');
}
});
delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
delegate(document, '.status__content__spoiler-link', 'click', function() {
const statusEl = this.parentNode.parentNode;
delegate(document, '.status__content__spoiler-link', 'click', function() {
const statusEl = this.parentNode.parentNode;
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
}
if (statusEl.dataset.spoiler === 'expanded') {
statusEl.dataset.spoiler = 'folded';
this.textContent = (new IntlMessageFormat(localeData['status.show_more'] || 'Show more', locale)).format();
} else {
statusEl.dataset.spoiler = 'expanded';
this.textContent = (new IntlMessageFormat(localeData['status.show_less'] || 'Show less', locale)).format();
}
return false;
});
return false;
});
[].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
const statusEl = spoilerLink.parentNode.parentNode;
const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
[].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => {
const statusEl = spoilerLink.parentNode.parentNode;
const message = (statusEl.dataset.spoiler === 'expanded') ? (localeData['status.show_less'] || 'Show less') : (localeData['status.show_more'] || 'Show more');
spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format();
});
const toggleSidebar = () => {

@ -13,4 +13,30 @@ ready(() => {
console.error(error);
});
}, 5000);
document.querySelectorAll('.timer-button').forEach(button => {
let counter = 30;
const container = document.createElement('span');
const updateCounter = () => {
container.innerText = ` (${counter})`;
};
updateCounter();
const countdown = setInterval(() => {
counter--;
if (counter === 0) {
button.disabled = false;
button.removeChild(container);
clearInterval(countdown);
} else {
updateCounter();
}
}, 1000);
button.appendChild(container);
});
});

@ -1038,3 +1038,33 @@ $ui-header-height: 55px;
}
}
}
.hashtag-header {
border-bottom: 1px solid lighten($ui-base-color, 8%);
padding: 15px;
font-size: 17px;
line-height: 22px;
color: $darker-text-color;
strong {
font-weight: 700;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
gap: 15px;
h1 {
color: $primary-text-color;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 22px;
line-height: 33px;
font-weight: 700;
}
}
}

@ -147,10 +147,6 @@
display: block;
width: 100%;
}
.layout-multiple-columns &.button--with-bell {
font-size: 12px;
}
}
.icon-button {
@ -1345,7 +1341,7 @@ button.icon-button.active i.fa-retweet {
display: flex;
align-items: center;
justify-content: center;
background: rgba($black, 0.5);
background: transparent;
width: 100%;
height: 100%;
padding: 0;
@ -1354,6 +1350,10 @@ button.icon-button.active i.fa-retweet {
color: $white;
&__label {
background-color: rgba($black, 0.45);
backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
border-radius: 6px;
padding: 10px 15px;
display: flex;
align-items: center;
justify-content: center;
@ -1367,6 +1367,13 @@ button.icon-button.active i.fa-retweet {
font-weight: 400;
font-size: 13px;
}
&:hover,
&:focus {
.spoiler-button__overlay__label {
background-color: rgba($black, 0.9);
}
}
}
}

@ -719,15 +719,16 @@
}
.button.button-secondary {
border-color: $ui-button-secondary-border-color;
color: $ui-button-secondary-color;
border-color: $inverted-text-color;
color: $inverted-text-color;
flex: 0 0 auto;
&:hover,
&:focus,
&:active {
border-color: $ui-button-secondary-focus-background-color;
color: $ui-button-secondary-focus-color;
background: transparent;
border-color: $ui-button-background-color;
color: $ui-button-background-color;
}
}
@ -1412,6 +1413,44 @@ img.modal-warning {
}
}
&__choices {
display: flex;
gap: 40px;
&__choice {
flex: 1;
box-sizing: border-box;
h3 {
margin-bottom: 20px;
}
p {
color: $darker-text-color;
margin-bottom: 20px;
font-size: 15px;
}
.button {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
}
@media screen and (max-width: $no-gap-breakpoint - 1px) {
&__choices {
flex-direction: column;
&__choice {
margin-top: 40px;
}
}
}
.link-button {
font-size: inherit;
display: inline;

@ -159,6 +159,7 @@
&.active {
transform: rotate(90deg);
opacity: 1;
}
&:hover {

@ -310,9 +310,19 @@ code {
border-radius: 4px;
background: url('images/void.png');
&[src$='missing.png'] {
visibility: hidden;
}
&:last-child {
margin-bottom: 0;
}
&#account_avatar-preview {
width: 90px;
height: 90px;
object-fit: cover;
}
}
}

@ -421,6 +421,10 @@ html {
border-top: 0;
}
.column-settings__hashtags .column-select__option {
color: $white;
}
.dashboard__quick-access,
.focal-point__preview strong,
.admin-wrapper .content__heading__tabs a.selected {

@ -76,7 +76,10 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
normalStatus.spoiler_text = normalOldStatus.get('spoiler_text');
normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.translation = normalOldStatus.get('translation');
if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation');
}
} else {
// If the status has a CW but no contents, treat the CW as if it were the
// status' contents, to avoid having a CW toggle with seemingly no effect.

@ -145,7 +145,7 @@ export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId });
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

@ -0,0 +1,199 @@
import { fromJS } from 'immutable';
import type { StatusLike } from '../hashtag_bar';
import { computeHashtagBarForStatus } from '../hashtag_bar';
function createStatus(
content: string,
hashtags: string[],
hasMedia = false,
spoilerText?: string,
) {
return fromJS({
tags: hashtags.map((name) => ({ name })),
contentHtml: content,
media_attachments: hasMedia ? ['fakeMedia'] : [],
spoiler_text: spoilerText,
}) as unknown as StatusLike; // need to force the type here, as it is not properly defined
}
describe('computeHashtagBarForStatus', () => {
it('does nothing when there are no tags', () => {
const status = createStatus('<p>Simple text</p>', []);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text</p>"`,
);
});
it('displays out of band hashtags in the bar', () => {
const status = createStatus(
'<p>Simple text <a href="test">#hashtag</a></p>',
['hashtag', 'test'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['test']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text <a href="test">#hashtag</a></p>"`,
);
});
it('extract tags from the last line', () => {
const status = createStatus(
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
['hashtag'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['hashtag']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text</p>"`,
);
});
it('does not include tags from content', () => {
const status = createStatus(
'<p>Simple text with a <a href="test">#hashtag</a></p><p><a href="test">#hashtag</a></p>',
['hashtag'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Simple text with a <a href="test">#hashtag</a></p>"`,
);
});
it('works with one line status and hashtags', () => {
const status = createStatus(
'<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>',
['hashtag', 'test'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a>. And another <a href="test">#hashtag</a></p>"`,
);
});
it('de-duplicate accentuated characters with case differences', () => {
const status = createStatus(
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
['éaa'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['Éaa']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text</p>"`,
);
});
it('handles server-side normalized tags with accentuated characters', () => {
const status = createStatus(
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
['eaa'], // The server may normalize the hashtags in the `tags` attribute
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['Éaa']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text</p>"`,
);
});
it('does not display in bar a hashtag in content with a case difference', () => {
const status = createStatus(
'<p>Text <a href="test">#Éaa</a></p><p><a href="test">#éaa</a></p>',
['éaa'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>Text <a href="test">#Éaa</a></p>"`,
);
});
it('does not modify a status with a line of hashtags only', () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
);
});
it('puts the hashtags in the bar if a status content has hashtags in the only line and has a media', () => {
const status = createStatus(
'<p>This is my content! <a href="test">#hashtag</a></p>',
['hashtag'],
true,
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p>This is my content! <a href="test">#hashtag</a></p>"`,
);
});
it('puts the hashtags in the bar if a status content is only hashtags and has a media', () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
true,
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual(['test', 'hashtag']);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(`""`);
});
it('does not use the hashtag bar if the status content is only hashtags, has a CW and a media', () => {
const status = createStatus(
'<p><a href="test">#test</a> <a href="test">#hashtag</a></p>',
['test', 'hashtag'],
true,
'My CW text',
);
const { hashtagsInBar, statusContentProps } =
computeHashtagBarForStatus(status);
expect(hashtagsInBar).toEqual([]);
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
`"<p><a href="test">#test</a> <a href="test">#hashtag</a></p>"`,
);
});
});

@ -8,6 +8,7 @@ import classNames from 'classnames';
import api from 'mastodon/api';
const messages = defineMessages({
legal: { id: 'report.categories.legal', defaultMessage: 'Legal' },
other: { id: 'report.categories.other', defaultMessage: 'Other' },
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
@ -150,6 +151,7 @@ class ReportReasonSelector extends PureComponent {
return (
<div className='report-reason-selector'>
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='legal' text={intl.formatMessage(messages.legal)} selected={category === 'legal'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}

@ -16,7 +16,19 @@ export default class Column extends PureComponent {
};
scrollTop () {
const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
let scrollable = null;
if (this.props.bindToDocument) {
scrollable = document.scrollingElement;
} else {
scrollable = this.node.querySelector('.scrollable');
// Some columns have nested `.scrollable` containers, with the outer one
// being a wrapper while the actual scrollable content is deeper.
if (scrollable.classList.contains('scrollable--flex')) {
scrollable = scrollable?.querySelector('.scrollable') || scrollable;
}
}
if (!scrollable) {
return;

@ -0,0 +1,234 @@
import { useState, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import type { List, Record } from 'immutable';
import { groupBy, minBy } from 'lodash';
import { getStatusContent } from './status_content';
// About two lines on desktop
const VISIBLE_HASHTAGS = 7;
// Those types are not correct, they need to be replaced once this part of the state is typed
export type TagLike = Record<{ name: string }>;
export type StatusLike = Record<{
tags: List<TagLike>;
contentHTML: string;
media_attachments: List<unknown>;
spoiler_text?: string;
}>;
function normalizeHashtag(hashtag: string) {
return (
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
).normalize('NFKC');
}
function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
return (
element instanceof HTMLAnchorElement &&
// it may be a <a> starting with a hashtag
(element.textContent?.[0] === '#' ||
// or a #<a>
element.previousSibling?.textContent?.[
element.previousSibling.textContent.length - 1
] === '#')
);
}
/**
* Removes duplicates from an hashtag list, case-insensitive, keeping only the best one
* "Best" here is defined by the one with the more casing difference (ie, the most camel-cased one)
* @param hashtags The list of hashtags
* @returns The input hashtags, but with only 1 occurence of each (case-insensitive)
*/
function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
const groups = groupBy(hashtags, (tag) =>
tag.normalize('NFKD').toLowerCase(),
);
return Object.values(groups).map((tags) => {
if (tags.length === 1) return tags[0];
// The best match is the one where we have the less difference between upper and lower case letter count
const best = minBy(tags, (tag) => {
const upperCase = Array.from(tag).reduce(
(acc, char) => (acc += char.toUpperCase() === char ? 1 : 0),
0,
);
const lowerCase = tag.length - upperCase;
return Math.abs(lowerCase - upperCase);
});
return best ?? tags[0];
});
}
// Create the collator once, this is much more efficient
const collator = new Intl.Collator(undefined, {
sensitivity: 'base', // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
});
function localeAwareInclude(collection: string[], value: string) {
const normalizedValue = value.normalize('NFKC');
return !!collection.find(
(item) => collator.compare(item.normalize('NFKC'), normalizedValue) === 0,
);
}
// We use an intermediate function here to make it easier to test
export function computeHashtagBarForStatus(status: StatusLike): {
statusContentProps: { statusContent: string };
hashtagsInBar: string[];
} {
let statusContent = getStatusContent(status);
const tagNames = status
.get('tags')
.map((tag) => tag.get('name'))
.toJS();
// this is returned if we stop the processing early, it does not change what is displayed
const defaultResult = {
statusContentProps: { statusContent },
hashtagsInBar: [],
};
// return early if this status does not have any tags
if (tagNames.length === 0) return defaultResult;
const template = document.createElement('template');
template.innerHTML = statusContent.trim();
const lastChild = template.content.lastChild;
if (!lastChild) return defaultResult;
template.content.removeChild(lastChild);
const contentWithoutLastLine = template;
// First, try to parse
const contentHashtags = Array.from(
contentWithoutLastLine.content.querySelectorAll<HTMLLinkElement>('a[href]'),
).reduce<string[]>((result, link) => {
if (isNodeLinkHashtag(link)) {
if (link.textContent) result.push(normalizeHashtag(link.textContent));
}
return result;
}, []);
// Now we parse the last line, and try to see if it only contains hashtags
const lastLineHashtags: string[] = [];
// try to see if the last line is only hashtags
let onlyHashtags = true;
const normalizedTagNames = tagNames.map((tag) => tag.normalize('NFKC'));
Array.from(lastChild.childNodes).forEach((node) => {
if (isNodeLinkHashtag(node) && node.textContent) {
const normalized = normalizeHashtag(node.textContent);
if (!localeAwareInclude(normalizedTagNames, normalized)) {
// stop here, this is not a real hashtag, so consider it as text
onlyHashtags = false;
return;
}
if (!localeAwareInclude(contentHashtags, normalized))
// only add it if it does not appear in the rest of the content
lastLineHashtags.push(normalized);
} else if (node.nodeType !== Node.TEXT_NODE || node.nodeValue?.trim()) {
// not a space
onlyHashtags = false;
}
});
const hashtagsInBar = tagNames.filter((tag) => {
const normalizedTag = tag.normalize('NFKC');
// the tag does not appear at all in the status content, it is an out-of-band tag
return (
!localeAwareInclude(contentHashtags, normalizedTag) &&
!localeAwareInclude(lastLineHashtags, normalizedTag)
);
});
const isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
const hasMedia = status.get('media_attachments').size > 0;
const hasSpoiler = !!status.get('spoiler_text');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- due to https://github.com/microsoft/TypeScript/issues/9998
if (onlyHashtags && ((hasMedia && !hasSpoiler) || !isOnlyOneLine)) {
// if the last line only contains hashtags, and we either:
// - have other content in the status
// - dont have other content, but a media and no CW. If it has a CW, then we do not remove the content to avoid having an empty content behind the CW button
statusContent = contentWithoutLastLine.innerHTML;
// and add the tags to the bar
hashtagsInBar.push(...lastLineHashtags);
}
return {
statusContentProps: { statusContent },
hashtagsInBar: uniqueHashtagsWithCaseHandling(hashtagsInBar),
};
}
/**
* This function will process a status to, at the same time (avoiding parsing it twice):
* - build the HashtagBar for this status
* - remove the last-line hashtags from the status content
* @param status The status to process
* @returns Props to be passed to the <StatusContent> component, and the hashtagBar to render
*/
export function getHashtagBarForStatus(status: StatusLike) {
const { statusContentProps, hashtagsInBar } =
computeHashtagBarForStatus(status);
return {
statusContentProps,
hashtagBar: <HashtagBar hashtags={hashtagsInBar} />,
};
}
const HashtagBar: React.FC<{
hashtags: string[];
}> = ({ hashtags }) => {
const [expanded, setExpanded] = useState(false);
const handleClick = useCallback(() => {
setExpanded(true);
}, []);
if (hashtags.length === 0) {
return null;
}
const revealedHashtags = expanded
? hashtags
: hashtags.slice(0, VISIBLE_HASHTAGS - 1);
return (
<div className='hashtag-bar'>
{revealedHashtags.map((hashtag) => (
<Link key={hashtag} to={`/tags/${hashtag}`}>
#<span>{hashtag}</span>
</Link>
))}
{!expanded && hashtags.length > VISIBLE_HASHTAGS && (
<button className='link-button' onClick={handleClick}>
<FormattedMessage
id='hashtags.and_other'
defaultMessage='…and {count, plural, other {# more}}'
values={{ count: hashtags.length - VISIBLE_HASHTAGS }}
/>
</button>
)}
</div>
);
};

@ -22,6 +22,7 @@ import { displayMedia } from '../initial_state';
import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name';
import { getHashtagBarForStatus } from './hashtag_bar';
import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';
@ -544,6 +545,9 @@ class Status extends ImmutablePureComponent {
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden')
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
@ -571,15 +575,18 @@ class Status extends ImmutablePureComponent {
<StatusContent
status={status}
onClick={this.handleClick}
expanded={!status.get('hidden')}
expanded={expanded}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
collapsible
onCollapsedToggle={this.handleCollapsedToggle}
{...statusContentProps}
/>
{media}
{expanded && hashtagBar}
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
</div>
</div>

@ -15,6 +15,15 @@ import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_s
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
/**
*
* @param {any} status
* @returns {string}
*/
export function getStatusContent(status) {
return status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
}
class TranslateButton extends PureComponent {
static propTypes = {
@ -65,6 +74,7 @@ class StatusContent extends PureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
statusContent: PropTypes.string,
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onTranslate: PropTypes.func,
@ -225,7 +235,7 @@ class StatusContent extends PureComponent {
};
render () {
const { status, intl } = this.props;
const { status, intl, statusContent } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
@ -233,7 +243,7 @@ class StatusContent extends PureComponent {
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
const content = { __html: statusContent ?? getStatusContent(status) };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {

@ -265,9 +265,9 @@ class Header extends ImmutablePureComponent {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames({ 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}

@ -22,6 +22,7 @@ export default class Story extends PureComponent {
author: PropTypes.string,
sharedTimes: PropTypes.number,
thumbnail: PropTypes.string,
thumbnailDescription: PropTypes.string,
blurhash: PropTypes.string,
expanded: PropTypes.bool,
};
@ -33,7 +34,7 @@ export default class Story extends PureComponent {
handleImageLoad = () => this.setState({ thumbnailLoaded: true });
render () {
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, blurhash } = this.props;
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props;
const { thumbnailLoaded } = this.state;
@ -49,7 +50,7 @@ export default class Story extends PureComponent {
{thumbnail ? (
<>
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
<img src={thumbnail} onLoad={this.handleImageLoad} alt='' role='presentation' />
<img src={thumbnail} onLoad={this.handleImageLoad} alt={thumbnailDescription} title={thumbnailDescription} lang={lang} />
</>
) : <Skeleton />}
</div>

@ -67,6 +67,7 @@ class Links extends PureComponent {
author={link.get('author_name')}
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
thumbnail={link.get('image')}
thumbnailDescription={link.get('image_description')}
blurhash={link.get('blurhash')}
/>
))}

@ -108,10 +108,10 @@ class Results extends PureComponent {
return (
<>
<div className='account__section-headline'>
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
<button onClick={this.handleSelectAll} className={type === 'all' ? 'active' : undefined}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' ? 'active' : undefined}><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' ? 'active' : undefined}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' ? 'active' : undefined}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
</div>
<div className='explore__search-results'>

@ -0,0 +1,79 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Button from 'mastodon/components/button';
import { ShortNumber } from 'mastodon/components/short_number';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
});
const usesRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const peopleRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const usesTodayRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses_today'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => {
if (!tag) {
return null;
}
const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]);
const dividingCircle = <span aria-hidden>{' · '}</span>;
return (
<div className='hashtag-header'>
<div className='hashtag-header__header'>
<h1>#{tag.get('name')}</h1>
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
</div>
<div>
<ShortNumber value={uses} renderer={usesRenderer} />
{dividingCircle}
<ShortNumber value={people} renderer={peopleRenderer} />
{dividingCircle}
<ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} />
</div>
</div>
);
});
HashtagHeader.propTypes = {
tag: ImmutablePropTypes.map,
disabled: PropTypes.bool,
onClick: PropTypes.func,
intl: PropTypes.object,
};

@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes';
@ -17,17 +16,12 @@ import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/t
import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import StatusListContainer from '../ui/containers/status_list_container';
import { HashtagHeader } from './components/hashtag_header';
import ColumnSettingsContainer from './containers/column_settings_container';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
});
const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
tag: state.getIn(['tags', props.params.id]),
@ -48,7 +42,6 @@ class HashtagTimeline extends PureComponent {
hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
intl: PropTypes.object,
};
handlePin = () => {
@ -188,27 +181,11 @@ class HashtagTimeline extends PureComponent {
};
render () {
const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
const { hasUnread, columnId, multiColumn, tag } = this.props;
const { id, local } = this.props.params;
const pinned = !!columnId;
const { signedIn } = this.context.identity;
let followButton;
if (tag) {
const following = tag.get('following');
const classes = classNames('column-header__button', {
active: following,
});
followButton = (
<button className={classes} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
</button>
);
}
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
<ColumnHeader
@ -220,13 +197,14 @@ class HashtagTimeline extends PureComponent {
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
extraButton={followButton}
showBackButton
>
{columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader>
<StatusListContainer
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}${local ? ':local' : ''}`}
@ -245,4 +223,4 @@ class HashtagTimeline extends PureComponent {
}
export default connect(mapStateToProps)(injectIntl(HashtagTimeline));
export default connect(mapStateToProps)(HashtagTimeline);

@ -13,7 +13,7 @@ import { openModal, closeModal } from 'mastodon/actions/modal';
import api from 'mastodon/api';
import Button from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
import { registrationsOpen } from 'mastodon/initial_state';
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
const messages = defineMessages({
loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' },
@ -21,12 +21,16 @@ const messages = defineMessages({
const mapStateToProps = (state, { accountId }) => ({
displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up',
});
const mapDispatchToProps = (dispatch) => ({
onSignupClick() {
dispatch(closeModal());
dispatch(openModal('CLOSED_REGISTRATIONS'));
dispatch(closeModal({
modalType: undefined,
ignoreFocus: false,
}));
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' }));
},
});
@ -294,6 +298,7 @@ class InteractionModal extends React.PureComponent {
url: PropTypes.string,
type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']),
onSignupClick: PropTypes.func.isRequired,
signupUrl: PropTypes.string.isRequired,
};
handleSignupClick = () => {
@ -301,7 +306,7 @@ class InteractionModal extends React.PureComponent {
};
render () {
const { url, type, displayNameHtml } = this.props;
const { url, type, displayNameHtml, signupUrl } = this.props;
const name = <bdi dangerouslySetInnerHTML={{ __html: displayNameHtml }} />;
@ -332,9 +337,15 @@ class InteractionModal extends React.PureComponent {
let signupButton;
if (registrationsOpen) {
if (sso_redirect) {
signupButton = (
<a href='/auth/sign_up' className='link-button'>
<a href={sso_redirect} data-method='post' className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);

@ -167,7 +167,8 @@ export default class Card extends PureComponent {
/>
);
let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
const thumbnailDescription = card.get('image_description');
const thumbnail = <img src={card.get('image')} alt={thumbnailDescription} title={thumbnailDescription} lang={language} style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>

@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { AnimatedNumber } from 'mastodon/components/animated_number';
import EditedTimestamp from 'mastodon/components/edited_timestamp';
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
import { Icon } from 'mastodon/components/icon';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
@ -291,6 +292,9 @@ class DetailedStatus extends ImmutablePureComponent {
);
}
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden')
return (
<div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
@ -310,10 +314,13 @@ class DetailedStatus extends ImmutablePureComponent {
expanded={!status.get('hidden')}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
{...statusContentProps}
/>
{media}
{expanded && hashtagBar}
<div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />

@ -571,7 +571,7 @@ class Status extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 && list.get(i - 1)}
previousId={i > 0 ? list.get(i - 1) : undefined}
nextId={list.get(i + 1) || (ancestors && statusId)}
rootId={statusId}
/>

@ -434,4 +434,4 @@ class FocalPointModal extends ImmutablePureComponent {
export default connect(mapStateToProps, mapDispatchToProps, null, {
forwardRef: true,
})(injectIntl(FocalPointModal, { withRef: true }));
})(injectIntl(FocalPointModal, { forwardRef: true }));

@ -12,7 +12,7 @@ import { fetchServer } from 'mastodon/actions/server';
import { Avatar } from 'mastodon/components/avatar';
import { Icon } from 'mastodon/components/icon';
import { WordmarkLogo, SymbolLogo } from 'mastodon/components/logo';
import { registrationsOpen, me } from 'mastodon/initial_state';
import { registrationsOpen, me, sso_redirect } from 'mastodon/initial_state';
const Account = connect(state => ({
account: state.getIn(['accounts', me]),
@ -73,28 +73,35 @@ class Header extends PureComponent {
</>
);
} else {
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
if (sso_redirect) {
content = (
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
)
} else {
signupButton = (
<button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
}
content = (
<>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</>
);
}
content = (
<>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</>
);
}
return (

@ -97,14 +97,7 @@ export default class ModalRoot extends PureComponent {
handleClose = (ignoreFocus = false) => {
const { onClose } = this.props;
let message = null;
try {
message = this._modal?.getWrappedInstance?.().getCloseConfirmationMessage?.();
} catch (_) {
// injectIntl defines `getWrappedInstance` but errors out if `withRef`
// isn't set.
// This would be much smoother with react-intl 3+ and `forwardRef`.
}
const message = this._modal?.getCloseConfirmationMessage?.();
onClose(message, ignoreFocus);
};
@ -122,7 +115,10 @@ export default class ModalRoot extends PureComponent {
{visible && (
<>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}>
{(SpecificComponent) => <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={this.setModalRef} />}
{(SpecificComponent) => {
const ref = typeof SpecificComponent !== 'function' ? this.setModalRef : undefined;
return <SpecificComponent {...props} onChangeBackgroundColor={this.setBackgroundColor} onClose={this.handleClose} ref={ref} />
}}
</BundleContainer>
<Helmet>

@ -62,7 +62,7 @@ class ReportModal extends ImmutablePureComponent {
dispatch(submitReport({
account_id: accountId,
status_ids: selectedStatusIds.toArray(),
selected_domains: selectedDomains.toArray(),
forward_to_domains: selectedDomains.toArray(),
comment,
forward: selectedDomains.size > 0,
category,

@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
import { openModal } from 'mastodon/actions/modal';
import { registrationsOpen } from 'mastodon/initial_state';
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const SignInBanner = () => {
@ -19,6 +19,15 @@ const SignInBanner = () => {
const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up');
if (sso_redirect) {
return (
<div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p>
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
</div>
)
}
if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='button button--block'>

@ -80,6 +80,7 @@
* @property {boolean} use_blurhash
* @property {boolean=} use_pending_items
* @property {string} version
* @property {string} sso_redirect
*/
/**
@ -142,6 +143,7 @@ export const version = getMeta('version');
export const languages = initialState?.languages;
// @ts-expect-error
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
// Glitch-soc-specific settings
export const maxChars = (initialState && initialState.max_toot_chars) || 500;

@ -181,7 +181,6 @@
"home.column_settings.show_reblogs": "Wys aangestuurde plasings",
"interaction_modal.description.reblog": "Met 'n rekening op Mastodon kan jy hierdie plasing aanstuur om dit met jou volgers te deel.",
"interaction_modal.description.reply": "Met 'n rekening op Mastodon kan jy op hierdie plasing reageer.",
"interaction_modal.preamble": "Omdat Mastodon gedesentraliseer is, hoef jy nie n rekening op hierdie bediener te hê nie. Jy kan jy jou bestaande Mastodonrekening gebruik, al word dit op 'n ander Mastodonbediener of versoenbare platform waar ook al gehuisves.",
"interaction_modal.title.reblog": "Stuur {name} se plasing aan",
"interaction_modal.title.reply": "Reageer op {name} se plasing",
"keyboard_shortcuts.back": "Navigeer terug",

@ -172,7 +172,6 @@
"conversation.open": "Veyer conversación",
"conversation.with": "Con {names}",
"copypaste.copied": "Copiau",
"copypaste.copy": "Copiar",
"directory.federated": "Dende lo fediverso conoixiu",
"directory.local": "Nomás de {domain}",
"directory.new_arrivals": "Recientment plegaus",
@ -276,7 +275,6 @@
"interaction_modal.description.reply": "Con una cuenta en Mastodon, puetz responder a esta publicación.",
"interaction_modal.on_another_server": "En un servidor diferent",
"interaction_modal.on_this_server": "En este servidor",
"interaction_modal.preamble": "Ya que Mastodon ye descentralizau, puetz usar la tuya cuenta existent alochada en unatro servidor Mastodon u plataforma compatible si no tiens una cuenta en este servidor.",
"interaction_modal.title.follow": "Seguir a {name}",
"interaction_modal.title.reblog": "Empentar la publicación de {name}",
"interaction_modal.title.reply": "Responder a la publicación de {name}",

@ -13,7 +13,7 @@
"about.rules": "قواعد الخادم",
"account.account_note_header": "مُلاحظة",
"account.add_or_remove_from_list": "الإضافة أو الإزالة من القائمة",
"account.badges.bot": "بوت",
"account.badges.bot": "آلي",
"account.badges.group": "فريق",
"account.block": "احجب @{name}",
"account.block_domain": "حظر اسم النِّطاق {domain}",
@ -181,6 +181,7 @@
"confirmations.mute.explanation": "هذا سيخفي المنشورات عنهم وتلك المشار فيها إليهم، لكنه سيسمح لهم برؤية منشوراتك ومتابعتك.",
"confirmations.mute.message": "هل أنت متأكد أنك تريد كتم {name} ؟",
"confirmations.redraft.confirm": "إزالة وإعادة الصياغة",
"confirmations.redraft.message": "هل أنت متأكد من أنك تريد حذف هذا المنشور و إعادة صياغته؟ سوف تفقد جميع الإعجابات و الترقيات أما الردود المتصلة به فستُصبِح يتيمة.",
"confirmations.reply.confirm": "رد",
"confirmations.reply.message": "الرد في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد كتابتها. متأكد من أنك تريد المواصلة؟",
"confirmations.unfollow.confirm": "إلغاء المتابعة",
@ -190,7 +191,6 @@
"conversation.open": "اعرض المحادثة",
"conversation.with": "مع {names}",
"copypaste.copied": "تم نسخه",
"copypaste.copy": "انسخ",
"copypaste.copy_to_clipboard": "نسخ إلى الحافظة",
"directory.federated": "مِن الفديفرس المعروف",
"directory.local": "مِن {domain} فقط",
@ -201,6 +201,7 @@
"dismissable_banner.community_timeline": "هذه هي أحدث المشاركات العامة من الأشخاص الذين تُستضاف حساباتهم على {domain}.",
"dismissable_banner.dismiss": "رفض",
"dismissable_banner.explore_links": "هذه القصص الإخبارية يتحدث عنها حاليًا أشخاص على هذا الخادم وكذا على الخوادم الأخرى للشبكة اللامركزية.",
"dismissable_banner.explore_statuses": "هذه هي المنشورات الرائجة على الشبكات الاجتماعيّة اليوم. تظهر المنشورات التي أعيد مشاركتها وحازت على مفضّلات أكثر في مرتبة عليا.",
"dismissable_banner.explore_tags": "هذه الوسوم تكتسب جذب اهتمام الناس حاليًا على هذا الخادم وكذا على الخوادم الأخرى للشبكة اللامركزية.",
"dismissable_banner.public_timeline": "هذه هي أحدث المنشورات العامة من الناس على الشبكة الاجتماعية التي يتبعها الناس على {domain}.",
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
@ -229,6 +230,8 @@
"empty_column.direct": "لم يتم الإشارة إليك بشكل خاص بعد. عندما تتلقى أو ترسل إشارة، سيتم عرضها هنا.",
"empty_column.domain_blocks": "ليس هناك نطاقات تم حجبها بعد.",
"empty_column.explore_statuses": "ليس هناك ما هو متداوَل الآن. عد في وقت لاحق!",
"empty_column.favourited_statuses": "ليس لديك أية منشورات مفضلة بعد. عندما ستقوم بالإعجاب بواحدة، ستظهر هنا.",
"empty_column.favourites": "لم يقم أي أحد بالإعجاب بهذا المنشور بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.",
"empty_column.follow_requests": "ليس عندك أي طلب للمتابعة بعد. سوف تظهر طلباتك هنا إن قمت بتلقي البعض منها.",
"empty_column.followed_tags": "لم تُتابع أي وسم بعدُ. ستظهر الوسوم هنا حينما تفعل ذلك.",
"empty_column.hashtag": "ليس هناك بعدُ أي محتوى ذو علاقة بهذا الوسم.",
@ -299,16 +302,22 @@
"home.column_settings.basic": "الأساسية",
"home.column_settings.show_reblogs": "اعرض الترقيات",
"home.column_settings.show_replies": "اعرض الردود",
"home.explore_prompt.body": "سوف تحتوي تغذية منزلك على مزيج من المشاركات من الوسوم التي اخترت متابعتها، والأشخاص الذين اخترت متابعتهم، والمشاركات التي قاموا بدعمها. الأمور تبدو هادئة جدا الآن، لذلك ماذا عن:",
"home.explore_prompt.body": "سوف يحتوي خيط أخبارك الرئيسي على مزيج من المشاركات من الوسوم التي اخترت متابعتها، والأشخاص الذين اخترت متابعتهم، والمنشورات التي قاموا بدعمها. ومع ذلك، إن كانت تبدو الأمور هادئة جدا، ماذا لو:",
"home.explore_prompt.title": "هذا مقرك الرئيسي داخل ماستدون.",
"home.hide_announcements": "إخفاء الإعلانات",
"home.show_announcements": "إظهار الإعلانات",
"interaction_modal.description.favourite": "بفضل حساب على ماستدون، يمكنك إضافة هذا المنشور إلى مفضلتك لإبلاغ الناشر عن تقديرك وكذا للاحتفاظ بالمنشور إلى وقت لاحق.",
"interaction_modal.description.follow": "مع حساب في ماستدون، يمكنك متابعة {name} وتلقي منشوراته على خيطك الرئيس.",
"interaction_modal.description.reblog": "مع حساب في ماستدون، يمكنك تعزيز هذا المنشور ومشاركته مع مُتابِعيك.",
"interaction_modal.description.reply": "مع حساب في ماستدون، يمكنك الرد على هذا المنشور.",
"interaction_modal.login.action": "خذني إلى خادمي",
"interaction_modal.login.prompt": "نطاق الخادم الخاص بك، على سبيل المثال mastodon.social",
"interaction_modal.no_account_yet": "ليست على ماستدون بعد؟",
"interaction_modal.on_another_server": "على خادم مختلف",
"interaction_modal.on_this_server": "على هذا الخادم",
"interaction_modal.preamble": "بما إن ماستدون لامركزي، يمكنك استخدام حسابك الحالي المستضاف بواسطة خادم ماستدون آخر أو منصة متوافقة إذا لم يكن لديك حساب هنا.",
"interaction_modal.sign_in": "لم تقم بتسجيل الدخول إلى هذا الخادم. أين هو مستضاف حسابك؟",
"interaction_modal.sign_in_hint": "تلميح: هذا هو الموقع الذي سجّلت عن طريقه. إن لم تتذكّر/ين اسم الموقع، يمكنك البحث عن الرسالة الترحيبيّة في بريدك الالكتروني. يمكنك أيضاً استخدام إسم المستخدم/ـة الكامل! (مثلاً: @Mastadon@mastadon.social)",
"interaction_modal.title.favourite": "إضافة منشور {name} إلى المفضلة",
"interaction_modal.title.follow": "اتبع {name}",
"interaction_modal.title.reblog": "مشاركة منشور {name}",
"interaction_modal.title.reply": "الرد على منشور {name}",
@ -324,6 +333,8 @@
"keyboard_shortcuts.direct": "to open direct messages column",
"keyboard_shortcuts.down": "للانتقال إلى أسفل القائمة",
"keyboard_shortcuts.enter": "لفتح المنشور",
"keyboard_shortcuts.favourite": "لإضافة المنشور إلى المفضلة",
"keyboard_shortcuts.favourites": "لفتح قائمة المفضلات",
"keyboard_shortcuts.federated": "لفتح الخيط الزمني الفديرالي",
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "لفتح الخيط الرئيسي",
@ -354,6 +365,7 @@
"lightbox.previous": "العودة",
"limited_account_hint.action": "إظهار الملف التعريفي على أي حال",
"limited_account_hint.title": "تم إخفاء هذا الملف الشخصي من قبل مشرفي {domain}.",
"link_preview.author": "مِن {name}",
"lists.account.add": "أضف إلى القائمة",
"lists.account.remove": "احذف من القائمة",
"lists.delete": "احذف القائمة",
@ -376,6 +388,7 @@
"mute_modal.hide_notifications": "هل تود إخفاء الإخطارات القادمة من هذا المستخدم ؟",
"mute_modal.indefinite": "إلى أجل غير مسمى",
"navigation_bar.about": "عن",
"navigation_bar.advanced_interface": "افتحه في واجهة الويب المتقدمة",
"navigation_bar.blocks": "الحسابات المحجوبة",
"navigation_bar.bookmarks": "الفواصل المرجعية",
"navigation_bar.community_timeline": "الخيط المحلي",
@ -385,6 +398,7 @@
"navigation_bar.domain_blocks": "النطاقات المحظورة",
"navigation_bar.edit_profile": "عدّل الملف التعريفي",
"navigation_bar.explore": "استكشف",
"navigation_bar.favourites": "المفضلة",
"navigation_bar.filters": "الكلمات المكتومة",
"navigation_bar.follow_requests": "طلبات المتابعة",
"navigation_bar.followed_tags": "الوسوم المتابَعة",
@ -401,6 +415,7 @@
"not_signed_in_indicator.not_signed_in": "تحتاج إلى تسجيل الدخول للوصول إلى هذا المصدر.",
"notification.admin.report": "{name} أبلغ عن {target}",
"notification.admin.sign_up": "أنشأ {name} حسابًا",
"notification.favourite": "أضاف {name} منشورك إلى مفضلته",
"notification.follow": "{name} يتابعك",
"notification.follow_request": "لقد طلب {name} متابعتك",
"notification.mention": "{name} ذكرك",
@ -414,6 +429,7 @@
"notifications.column_settings.admin.report": "التقارير الجديدة:",
"notifications.column_settings.admin.sign_up": "التسجيلات الجديدة:",
"notifications.column_settings.alert": "إشعارات سطح المكتب",
"notifications.column_settings.favourite": "المفضلة:",
"notifications.column_settings.filter_bar.advanced": "اعرض كافة الفئات",
"notifications.column_settings.filter_bar.category": "شريط الفلترة السريعة",
"notifications.column_settings.filter_bar.show_bar": "إظهار شريط التصفية",
@ -431,6 +447,7 @@
"notifications.column_settings.update": "التعديلات:",
"notifications.filter.all": "الكل",
"notifications.filter.boosts": "الترقيات",
"notifications.filter.favourites": "المفضلة",
"notifications.filter.follows": "يتابِع",
"notifications.filter.mentions": "الإشارات",
"notifications.filter.polls": "نتائج استطلاع الرأي",
@ -581,6 +598,8 @@
"server_banner.server_stats": "إحصائيات الخادم:",
"sign_in_banner.create_account": "أنشئ حسابًا",
"sign_in_banner.sign_in": "تسجيل الدخول",
"sign_in_banner.sso_redirect": "تسجيل الدخول أو إنشاء حساب",
"sign_in_banner.text": "قم بالولوج بحسابك لمتابعة الصفحات الشخصية أو الوسوم، أو لإضافة المنشورات إلى المفضلة ومشاركتها والرد عليها أو التفاعل بواسطة حسابك المتواجد على خادم مختلف.",
"status.admin_account": "افتح الواجهة الإدارية لـ @{name}",
"status.admin_domain": "فتح واجهة الإشراف لـ {domain}",
"status.admin_status": "افتح هذا المنشور على واجهة الإشراف",
@ -597,6 +616,7 @@
"status.edited": "عُدّل في {date}",
"status.edited_x_times": "عُدّل {count, plural, zero {} one {مرةً واحدة} two {مرّتان} few {{count} مرات} many {{count} مرة} other {{count} مرة}}",
"status.embed": "إدماج",
"status.favourite": "فضّل",
"status.filter": "تصفية هذه الرسالة",
"status.filtered": "مُصفّى",
"status.hide": "إخفاء المنشور",

@ -133,7 +133,6 @@
"conversation.open": "Ver la conversación",
"conversation.with": "Con {names}",
"copypaste.copied": "Copióse",
"copypaste.copy": "Copiar",
"directory.federated": "Del fediversu conocíu",
"directory.local": "De «{domain}» namás",
"directory.new_arrivals": "Cuentes nueves",
@ -213,6 +212,7 @@
"hashtag.column_header.tag_mode.none": "ensin {additional}",
"hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
"hashtag.follow": "Siguir a la etiqueta",
"hashtag.unfollow": "Dexar de siguir a la etiqueta",
"home.column_settings.basic": "Configuración básica",
@ -223,7 +223,6 @@
"interaction_modal.description.reply": "Con una cuenta de Mastodon, pues responder a esti artículu.",
"interaction_modal.on_another_server": "N'otru sirvidor",
"interaction_modal.on_this_server": "Nesti sirvidor",
"interaction_modal.preamble": "Darréu que Mastodon ye una rede social descentralizada, pues usar una cuenta agospiada n'otru sirvidor de Mastodon o n'otra plataforma compatible si nun tienes cuenta nesti sirvidor.",
"interaction_modal.title.reply": "Rempuesta al artículu de: {name}",
"intervals.full.days": "{number, plural, one {# día} other {# díes}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
@ -420,6 +419,7 @@
"server_banner.learn_more": "Saber más",
"server_banner.server_stats": "Estadístiques del sirvidor:",
"sign_in_banner.create_account": "Crear una cuenta",
"sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase",
"status.admin_account": "Abrir la interfaz de moderación pa @{name}",
"status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»",
"status.admin_status": "Abrir esti artículu na interfaz de moderación",

@ -113,6 +113,7 @@
"column.direct": "Асабістыя згадванні",
"column.directory": "Праглядзець профілі",
"column.domain_blocks": "Заблакіраваныя дамены",
"column.favourites": "Упадабанае",
"column.firehose": "Стужкі",
"column.follow_requests": "Запыты на падпіску",
"column.home": "Галоўная",
@ -180,6 +181,7 @@
"confirmations.mute.explanation": "Гэта схавае допісы ад гэтага карыстальніка і пра яго, але ўсё яшчэ дазволіць яму чытаць вашыя допісы і быць падпісаным на вас.",
"confirmations.mute.message": "Вы ўпэўненыя, што хочаце ігнараваць {name}?",
"confirmations.redraft.confirm": "Выдаліць і перапісаць",
"confirmations.redraft.message": "Вы ўпэўнены, што хочаце выдаліць допіс і перапісаць яго? Упадабанні і пашырэнні згубяцца, а адказы да арыгінальнага допісу асірацеюць.",
"confirmations.reply.confirm": "Адказаць",
"confirmations.reply.message": "Калі вы адкажаце зараз, гэта ператрэ паведамленне, якое вы пішаце. Вы ўпэўнены, што хочаце працягнуць?",
"confirmations.unfollow.confirm": "Адпісацца",
@ -189,7 +191,6 @@
"conversation.open": "Прагледзець размову",
"conversation.with": "З {names}",
"copypaste.copied": "Скапіравана",
"copypaste.copy": "Скапіраваць",
"copypaste.copy_to_clipboard": "Капіраваць у буфер абмену",
"directory.federated": "З вядомага федэсвету",
"directory.local": "Толькі з {domain}",
@ -200,6 +201,7 @@
"dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.",
"dismissable_banner.dismiss": "Адхіліць",
"dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца прама зараз на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
"dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.",
"dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі",
"dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.",
"embed.instructions": "Убудуйце гэты пост на свой сайт, скапіраваўшы прыведзены ніжэй код",
@ -228,6 +230,8 @@
"empty_column.direct": "Пакуль у вас няма асабістых згадак. Калі вы дашляце або атрымаеце штось, яно з'явіцца тут.",
"empty_column.domain_blocks": "Заблакіраваных даменаў пакуль няма.",
"empty_column.explore_statuses": "Зараз не ў трэндзе. Праверце пазней",
"empty_column.favourited_statuses": "Вы яшчэ не ўпадабалі ніводны допіс. Калі гэта адбудзецца, вы ўбачыце яго тут.",
"empty_column.favourites": "Ніхто яшчэ не ўпадабаў гэты допіс. Калі гэта адбудзецца, вы ўбачыце гэтых людзей тут.",
"empty_column.follow_requests": "У вас яшчэ няма запытаў на падпіскуі. Калі вы атрымаеце запыт, ён з'явяцца тут.",
"empty_column.followed_tags": "Вы пакуль не падпісаны ні на адзін хэштэг. Калі падпішацеся, яны з'явяцца тут.",
"empty_column.hashtag": "Па гэтаму хэштэгу пакуль што нічога няма.",
@ -291,6 +295,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 {{counter} удзельнік} few {{counter} удзельніка} many {{counter} удзельнікаў} other {{counter} удзельніка}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}} за сёння",
"hashtag.follow": "Падпісацца на хэштэг",
"hashtag.unfollow": "Адпісацца ад хэштэга",
"home.actions.go_to_explore": "Паглядзіце, што ў трэндзе",
@ -302,12 +309,18 @@
"home.explore_prompt.title": "Гэта ваша апорная кропка ў Mastodon.",
"home.hide_announcements": "Схаваць аб'явы",
"home.show_announcements": "Паказаць аб'явы",
"interaction_modal.description.favourite": "Маючы ўліковы запіс Mastodon, вы можаце ўпадабаць гэты допіс, каб паведаміць аўтару, што ён вам падабаецца, і захаваць яго на будучыню.",
"interaction_modal.description.follow": "Маючы акаўнт у Mastodon, вы можаце падпісацца на {name}, каб бачыць яго/яе допісы ў сваёй хатняй стужцы.",
"interaction_modal.description.reblog": "З уліковым запісам Mastodon, вы можаце пашырыць гэты пост, каб падзяліцца ім са сваімі падпісчыкамі.",
"interaction_modal.description.reply": "Маючы акаўнт у Mastodon, вы можаце адказаць на гэты пост.",
"interaction_modal.login.action": "Вярніце мяне дадому",
"interaction_modal.login.prompt": "Дамен вашага хатняга сервера, напрыклад, mastodon.social",
"interaction_modal.no_account_yet": "Яшчэ не ў Mastodon?",
"interaction_modal.on_another_server": "На іншым серверы",
"interaction_modal.on_this_server": "На гэтым серверы",
"interaction_modal.preamble": "Паколькі Mastodon дэцэнтралізаваны, вы можаце выкарыстоўваць існуючы акаўнт, які быў створаны на іншым серверы Mastodon або на сумяшчальнай платформе, калі ў вас пакуль няма акаўнта тут.",
"interaction_modal.sign_in": "Вы не выканалі ўваход на гэтым серверы. Дзе размешчаны ваш уліковы запіс?",
"interaction_modal.sign_in_hint": "Падказка: гэта сайт, на якім вы зарэгістраваліся. Калі вы не памятаеце, знайдзіце ліст у паштовай скрыні. Вы таксама можаце ўвесці сваё поўнае імя карыстальніка! (напрыклад, @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Упадабаць допіс {name}",
"interaction_modal.title.follow": "Падпісацца на {name}",
"interaction_modal.title.reblog": "Пашырыць допіс ад {name}",
"interaction_modal.title.reply": "Адказаць на допіс {name}",
@ -323,6 +336,8 @@
"keyboard_shortcuts.direct": "адкрыць стоўп асабістых згадак",
"keyboard_shortcuts.down": "Перамясціцца ўніз па спісе",
"keyboard_shortcuts.enter": "Адкрыць допіс",
"keyboard_shortcuts.favourite": "Упадабаць допіс",
"keyboard_shortcuts.favourites": "Адкрыць спіс упадабанага",
"keyboard_shortcuts.federated": "Адкрыць інтэграваную стужку",
"keyboard_shortcuts.heading": "Спалучэнні клавіш",
"keyboard_shortcuts.home": "Адкрыць хатнюю храналагічную стужку",
@ -353,6 +368,7 @@
"lightbox.previous": "Назад",
"limited_account_hint.action": "Усе роўна паказваць профіль",
"limited_account_hint.title": "Гэты профіль быў схаваны мадэратарамі",
"link_preview.author": "Ад {name}",
"lists.account.add": "Дадаць да спісу",
"lists.account.remove": "Выдаліць са спісу",
"lists.delete": "Выдаліць спіс",
@ -375,6 +391,7 @@
"mute_modal.hide_notifications": "Схаваць апавяшчэнні ад гэтага карыстальніка?",
"mute_modal.indefinite": "Бестэрмінова",
"navigation_bar.about": "Пра нас",
"navigation_bar.advanced_interface": "Адкрыць у пашыраным вэб-інтэрфейсе",
"navigation_bar.blocks": "Заблакаваныя карыстальнікі",
"navigation_bar.bookmarks": "Закладкі",
"navigation_bar.community_timeline": "Лакальная стужка",
@ -384,6 +401,7 @@
"navigation_bar.domain_blocks": "Заблакіраваныя дамены",
"navigation_bar.edit_profile": "Рэдагаваць профіль",
"navigation_bar.explore": "Агляд",
"navigation_bar.favourites": "Упадабанае",
"navigation_bar.filters": "Ігнараваныя словы",
"navigation_bar.follow_requests": "Запыты на падпіску",
"navigation_bar.followed_tags": "Падпіскі",
@ -400,6 +418,7 @@
"not_signed_in_indicator.not_signed_in": "Вам трэба ўвайсці каб атрымаць доступ да гэтага рэсурсу.",
"notification.admin.report": "{name} паскардзіўся на {target}",
"notification.admin.sign_up": "{name} зарэгістраваўся",
"notification.favourite": "Ваш допіс упадабаны {name}",
"notification.follow": "{name} падпісаўся на вас",
"notification.follow_request": "{name} адправіў запыт на падпіску",
"notification.mention": "{name} згадаў вас",
@ -413,6 +432,7 @@
"notifications.column_settings.admin.report": "Новыя скаргі:",
"notifications.column_settings.admin.sign_up": "Новыя ўваходы:",
"notifications.column_settings.alert": "Апавяшчэнні на працоўным стале",
"notifications.column_settings.favourite": "Упадабанае:",
"notifications.column_settings.filter_bar.advanced": "Паказваць усе катэгорыі",
"notifications.column_settings.filter_bar.category": "Панэль хуткай фільтрацыі",
"notifications.column_settings.filter_bar.show_bar": "Паказваць панэль фільтрацыі",
@ -430,6 +450,7 @@
"notifications.column_settings.update": "Праўкі:",
"notifications.filter.all": "Усе",
"notifications.filter.boosts": "Пашырэнні",
"notifications.filter.favourites": "Упадабанае",
"notifications.filter.follows": "Падпісаны на",
"notifications.filter.mentions": "Згадванні",
"notifications.filter.polls": "Вынікі апытання",
@ -580,6 +601,8 @@
"server_banner.server_stats": "Статыстыка сервера:",
"sign_in_banner.create_account": "Стварыць уліковы запіс",
"sign_in_banner.sign_in": "Увайсці",
"sign_in_banner.sso_redirect": "Уваход ці рэгістрацыя",
"sign_in_banner.text": "Увайдзіце, каб падпісацца на людзей і тэгі, каб адказваць на допісы, дзяліцца імі і падабаць іх, альбо кантактаваць з вашага ўліковага запісу на іншым серверы.",
"status.admin_account": "Адкрыць інтэрфейс мадэратара для @{name}",
"status.admin_domain": "Адкрыць інтэрфейс мадэратара для {domain}",
"status.admin_status": "Адкрыць гэты допіс у інтэрфейсе мадэрацыі",
@ -596,6 +619,7 @@
"status.edited": "Адрэдагавана {date}",
"status.edited_x_times": "Рэдагавана {count, plural, one {{count} раз} few {{count} разы} many {{count} разоў} other {{count} разу}}",
"status.embed": "Убудаваць",
"status.favourite": "Упадабанае",
"status.filter": "Фільтраваць гэты допіс",
"status.filtered": "Адфільтравана",
"status.hide": "Схаваць допіс",

@ -189,7 +189,6 @@
"conversation.open": "Преглед на разговора",
"conversation.with": "С {names}",
"copypaste.copied": "Копирано",
"copypaste.copy": "Копиране",
"copypaste.copy_to_clipboard": "Копиране в буферната памет",
"directory.federated": "От позната федивселена",
"directory.local": "Само от {domain}",
@ -298,7 +297,6 @@
"home.column_settings.basic": "Основно",
"home.column_settings.show_reblogs": "Показване на подсилванията",
"home.column_settings.show_replies": "Показване на отговорите",
"home.explore_prompt.body": "Вашият начален инфоканал ще е смес на публикации от хаштаговете, които сте избрали да следвате, избраните хора за следвате, а и публикациите, които са подсилили. Изглежда доста тихо в момента, така че какво ще кажете за:",
"home.explore_prompt.title": "Това е началната ви база с Mastodon.",
"home.hide_announcements": "Скриване на оповестяванията",
"home.show_announcements": "Показване на оповестяванията",
@ -307,7 +305,6 @@
"interaction_modal.description.reply": "С акаунт в Mastodon може да добавите отговор към тази публикация.",
"interaction_modal.on_another_server": "На различен сървър",
"interaction_modal.on_this_server": "На този сървър",
"interaction_modal.preamble": "Откак Mastodon е децентрализиран, може да употребявате съществуващ акаунт, разположен на друг сървър на Mastodon или съвместима платформа, ако нямате акаунт на този сървър.",
"interaction_modal.title.follow": "Последване на {name}",
"interaction_modal.title.reblog": "Подсилване на публикацията на {name}",
"interaction_modal.title.reply": "Отговаряне на публикацията на {name}",

@ -17,6 +17,7 @@
"account.badges.group": "Strollad",
"account.block": "Stankañ @{name}",
"account.block_domain": "Stankañ an domani {domain}",
"account.block_short": "Stankañ",
"account.blocked": "Stanket",
"account.browse_more_on_origin_server": "Furchal pelloc'h war ar profil orin",
"account.cancel_follow_request": "Nullañ ar reked heuliañ",
@ -46,6 +47,8 @@
"account.mention": "Menegiñ @{name}",
"account.moved_to": "Gant {name} eo bet merket e oa bremañ h·e gont nevez :",
"account.mute": "Kuzhat @{name}",
"account.mute_notifications_short": "Kuzhat ar c'hemennoù",
"account.mute_short": "Kuzhat",
"account.muted": "Kuzhet",
"account.open_original_page": "Digeriñ ar bajenn orin",
"account.posts": "Toudoù",
@ -171,7 +174,6 @@
"conversation.open": "Gwelout ar gaozeadenn",
"conversation.with": "Gant {names}",
"copypaste.copied": "Eilet",
"copypaste.copy": "Eilañ",
"directory.federated": "Eus ar fedibed anavezet",
"directory.local": "Eus {domain} hepken",
"directory.new_arrivals": "Degouezhet a-nevez",

@ -80,7 +80,7 @@
"admin.impact_report.instance_followers": "Seguidors que els nostres usuaris perdrien",
"admin.impact_report.instance_follows": "Seguidors que els seus usuaris perdrien",
"admin.impact_report.title": "Resum del impacte",
"alert.rate_limited.message": "Si us plau prova-ho després de {retry_time, time, medium}.",
"alert.rate_limited.message": "Proveu-ho una altra vegada al cap de {retry_time, time, medium}.",
"alert.rate_limited.title": "Límit de freqüència",
"alert.unexpected.message": "S'ha produït un error inesperat.",
"alert.unexpected.title": "Vaja!",
@ -114,7 +114,7 @@
"column.directory": "Navega pels perfils",
"column.domain_blocks": "Dominis blocats",
"column.favourites": "Favorits",
"column.firehose": "Fluxos en directe",
"column.firehose": "Tuts en directe",
"column.follow_requests": "Peticions de seguir-te",
"column.home": "Inici",
"column.lists": "Llistes",
@ -138,11 +138,11 @@
"compose.published.body": "Tut publicat.",
"compose.published.open": "Obre",
"compose_form.direct_message_warning_learn_more": "Més informació",
"compose_form.encryption_warning": "Els tuts a Mastodon no estant xifrats punt a punt. No comparteixis informació sensible mitjançant Mastodon.",
"compose_form.encryption_warning": "Les publicacions a Mastodon no estant xifrades punt a punt. No comparteixis informació sensible mitjançant Mastodon.",
"compose_form.hashtag_warning": "Aquest tut no apareixerà a les llistes d'etiquetes perquè no és públic. Només els tuts públics apareixen a les cerques per etiqueta.",
"compose_form.lock_disclaimer": "El teu compte no està {locked}. Tothom pot seguir-te i veure els tuts de només per a seguidors.",
"compose_form.lock_disclaimer.lock": "blocat",
"compose_form.placeholder": "En què penses?",
"compose_form.placeholder": "Què tens al cap?",
"compose_form.poll.add_option": "Afegeix una opció",
"compose_form.poll.duration": "Durada de l'enquesta",
"compose_form.poll.option_placeholder": "Opció {number}",
@ -191,7 +191,6 @@
"conversation.open": "Mostra la conversa",
"conversation.with": "Amb {names}",
"copypaste.copied": "Copiat",
"copypaste.copy": "Copia",
"copypaste.copy_to_clipboard": "Copia al porta-retalls",
"directory.federated": "Del fedivers conegut",
"directory.local": "Només de {domain}",
@ -296,6 +295,9 @@
"hashtag.column_settings.tag_mode.any": "Qualsevol daquests",
"hashtag.column_settings.tag_mode.none": "Cap daquests",
"hashtag.column_settings.tag_toggle": "Inclou etiquetes addicionals per a aquesta columna",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participant} other {{counter} participants}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} tut} other {{counter} tuts}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} tut} other {{counter} tuts}} avui",
"hashtag.follow": "Segueix l'etiqueta",
"hashtag.unfollow": "Deixa de seguir l'etiqueta",
"home.actions.go_to_explore": "Mira què és tendència",
@ -303,7 +305,7 @@
"home.column_settings.basic": "Bàsic",
"home.column_settings.show_reblogs": "Mostra els impulsos",
"home.column_settings.show_replies": "Mostra les respostes",
"home.explore_prompt.body": "La teva línia de temps Inici tindrà una barreja dels tuts de les etiquetes que has triat seguir, de les persones que has triat seguir i dels tuts que impulsen. Ara mateix es veu força tranquila, què et sembla si:",
"home.explore_prompt.body": "La teva línia de temps Inici tindrà una barreja dels tuts de les etiquetes que has triat seguir, de les persones que has triat seguir i dels tuts que s'impulsen. Ara mateix es veu força tranquila, què et sembla si:",
"home.explore_prompt.title": "Aquest és la teva base a Mastodon.",
"home.hide_announcements": "Amaga els anuncis",
"home.show_announcements": "Mostra els anuncis",
@ -311,10 +313,13 @@
"interaction_modal.description.follow": "Amb un compte a Mastodon, pots seguir a {name} per a rebre els seus tuts en la teva línia de temps d'Inici.",
"interaction_modal.description.reblog": "Amb un compte a Mastodon, pots impulsar aquest tut per a compartir-lo amb els teus seguidors.",
"interaction_modal.description.reply": "Amb un compte a Mastodon, pots respondre aquest tut.",
"interaction_modal.login.action": "Torna a l'inici",
"interaction_modal.login.prompt": "Domini del teu servidor domèstic, p.ex. mastodon.social",
"interaction_modal.no_account_yet": "No a Mastodon?",
"interaction_modal.on_another_server": "En un servidor diferent",
"interaction_modal.on_this_server": "En aquest servidor",
"interaction_modal.other_server_instructions": "Copia i enganxa aquest URL en el camp de cerca de la teva aplicació Mastodon preferida o a la interfície web del teu servidor Mastodon.",
"interaction_modal.preamble": "Com que Mastodon és descentralitzat, pots fer servir el teu compte existent en un altre servidor Mastodon o plataforma compatible si no tens compte en aquest.",
"interaction_modal.sign_in": "No has iniciat sessió en aquest servidor. On tens el teu compte?",
"interaction_modal.sign_in_hint": "Ajuda: Aquesta és la web on vas registrar-te. Si no ho recordes, mira el correu electrònic de benvinguda en la teva safata d'entrada. També pots introduïr el teu nom d'usuari complet! (per ex. @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Afavoreix el tut de {name}",
"interaction_modal.title.follow": "Segueix {name}",
"interaction_modal.title.reblog": "Impulsa el tut de {name}",
@ -596,6 +601,7 @@
"server_banner.server_stats": "Estadístiques del servidor:",
"sign_in_banner.create_account": "Crea un compte",
"sign_in_banner.sign_in": "Inici de sessió",
"sign_in_banner.sso_redirect": "Inici de sessió o Registre",
"sign_in_banner.text": "Inicia la sessió per a seguir perfils o etiquetes, afavorir, compartir i respondre tuts. També pots interactuar des del teu compte a un servidor diferent.",
"status.admin_account": "Obre la interfície de moderació per a @{name}",
"status.admin_domain": "Obre la interfície de moderació per a @{domain}",

@ -176,7 +176,6 @@
"conversation.open": "نیشاندان گفتوگۆ",
"conversation.with": "لەگەڵ{names}",
"copypaste.copied": "کۆپی کراوە",
"copypaste.copy": "ڕوونووس",
"directory.federated": "لە ڕاژەکانی ناسراو",
"directory.local": "تەنها لە {domain}",
"directory.new_arrivals": "تازە گەیشتنەکان",
@ -284,7 +283,6 @@
"interaction_modal.description.reply": "بە هەژمارێک لەسەر ماستدۆن، ئەتوانیت وەڵامی ئەم بڵاوکراوەیە بدەیتەوە.",
"interaction_modal.on_another_server": "لەسەر ڕاژەیەکی جیا",
"interaction_modal.on_this_server": "لەسەر ئەم ڕاژەیە",
"interaction_modal.preamble": "بەو پێیەی ماستۆدۆن لامەرکەزییە، دەتوانیت ئەکاونتی ئێستات بەکاربهێنیت کە لەلایەن سێرڤەرێکی تری ماستۆدۆن یان پلاتفۆرمی گونجاوەوە هۆست کراوە ئەگەر ئەکاونتێکت لەسەر ئەم ئەکاونتە نەبێت.",
"interaction_modal.title.follow": "دوای {name} بکەوە",
"interaction_modal.title.reblog": "پۆستی {name} زیاد بکە",
"interaction_modal.title.reply": "وەڵامی پۆستەکەی {name} بدەرەوە",

@ -191,7 +191,6 @@
"conversation.open": "Zobrazit konverzaci",
"conversation.with": "S {names}",
"copypaste.copied": "Zkopírováno",
"copypaste.copy": "Zkopírovat",
"copypaste.copy_to_clipboard": "Zkopírovat do schránky",
"directory.federated": "Ze známého fedivesmíru",
"directory.local": "Pouze z {domain}",
@ -202,7 +201,9 @@
"dismissable_banner.community_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí, jejichž účty hostuje {domain}.",
"dismissable_banner.dismiss": "Zavřít",
"dismissable_banner.explore_links": "O těchto zprávách hovoří lidé na tomto a dalších serverech decentralizované sítě právě teď.",
"dismissable_banner.explore_statuses": "Toto jsou příspěvky ze sociálních sítí, které dnes získávají na popularitě. Novější příspěvky s větším počtem boostů a oblíbení jsou hodnoceny výše.",
"dismissable_banner.explore_tags": "Tyto hashtagy právě teď získávají na popularitě mezi lidmi na tomto a dalších serverech decentralizované sítě.",
"dismissable_banner.public_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí na sociální síti, které sledují lidé na {domain}.",
"embed.instructions": "Pro přidání příspěvku na vaši webovou stránku zkopírujte níže uvedený kód.",
"embed.preview": "Takhle to bude vypadat:",
"emoji_button.activity": "Aktivita",
@ -229,6 +230,8 @@
"empty_column.direct": "Zatím nemáte žádné soukromé zmínky. Až nějakou pošlete nebo dostanete, zobrazí se zde.",
"empty_column.domain_blocks": "Ještě nemáte žádné zablokované domény.",
"empty_column.explore_statuses": "Momentálně není nic populární. Vraťte se později!",
"empty_column.favourited_statuses": "Zatím nemáte žádné oblíbené příspěvky. Až si nějaký oblíbíte, zobrazí se zde.",
"empty_column.favourites": "Tento příspěvek si zatím nikdo neoblíbil. Až to někdo udělá, zobrazí se zde.",
"empty_column.follow_requests": "Zatím nemáte žádné žádosti o sledování. Až nějakou obdržíte, zobrazí se zde.",
"empty_column.followed_tags": "Zatím jste nesledovali žádné hashtagy. Až to uděláte, objeví se zde.",
"empty_column.hashtag": "Pod tímto hashtagem zde zatím nic není.",
@ -294,17 +297,27 @@
"hashtag.column_settings.tag_toggle": "Zahrnout v tomto sloupci další štítky",
"hashtag.follow": "Sledovat hashtag",
"hashtag.unfollow": "Přestat sledovat hashtag",
"home.actions.go_to_explore": "Podívejte se, co frčí",
"home.actions.go_to_suggestions": "Najít lidi ke sledování",
"home.column_settings.basic": "Základní",
"home.column_settings.show_reblogs": "Zobrazit boosty",
"home.column_settings.show_replies": "Zobrazit odpovědi",
"home.explore_prompt.body": "Váš domovský kanál bude obsahovat směs příspěvků z hashtagů, které jste se rozhodli sledovat, lidí, které jste se rozhodli sledovat, a příspěvků, které boostují. Pokud vám to připadá příliš klidné, možná budete chtít:",
"home.explore_prompt.title": "Toto je vaše domovská základna uvnitř Mastodonu.",
"home.hide_announcements": "Skrýt oznámení",
"home.show_announcements": "Zobrazit oznámení",
"interaction_modal.description.favourite": "Pokud máte účet na Mastodonu, můžete tento příspěvek označit jako oblíbený a dát tak autorovi najevo, že si ho vážíte, a uložit si ho na později.",
"interaction_modal.description.follow": "S účtem na Mastodonu můžete sledovat uživatele {name} a přijímat příspěvky ve vašem domovském kanálu.",
"interaction_modal.description.reblog": "S účtem na Mastodonu můžete boostnout tento příspěvek a sdílet jej s vlastními sledujícími.",
"interaction_modal.description.reply": "S účtem na Mastodonu můžete odpovědět na tento příspěvek.",
"interaction_modal.login.action": "Domů",
"interaction_modal.login.prompt": "Doména vašeho domovského serveru, např. mastodon.social",
"interaction_modal.no_account_yet": "Nejste na Mastodonu?",
"interaction_modal.on_another_server": "Na jiném serveru",
"interaction_modal.on_this_server": "Na tomto serveru",
"interaction_modal.preamble": "Protože Mastodon je decentralizovaný, pokud nemáte účet na tomto serveru, můžete použít svůj existující účet hostovaný jiným Mastodon serverem nebo kompatibilní platformou.",
"interaction_modal.sign_in": "Nejste přihlášeni k tomuto serveru. Kde je váš účet hostován?",
"interaction_modal.sign_in_hint": "Tip: To je stránka, na které jste se zaregistrovali. Pokud si ji nepamatujete, vyhledejte ve své e-mailové schránce uvítací e-mail. Můžete také zadat své celé uživatelské jméno! (např. @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Oblíbit si příspěvek od uživatele {name}",
"interaction_modal.title.follow": "Sledovat {name}",
"interaction_modal.title.reblog": "Boostnout příspěvek uživatele {name}",
"interaction_modal.title.reply": "Odpovědět na příspěvek uživatele {name}",
@ -320,6 +333,8 @@
"keyboard_shortcuts.direct": "otevřít sloupec soukromých zmínek",
"keyboard_shortcuts.down": "Posunout v seznamu dolů",
"keyboard_shortcuts.enter": "Otevřít příspěvek",
"keyboard_shortcuts.favourite": "Oblíbit si příspěvek",
"keyboard_shortcuts.favourites": "Otevřít seznam oblíbených",
"keyboard_shortcuts.federated": "Otevřít federovanou časovou osu",
"keyboard_shortcuts.heading": "Klávesové zkratky",
"keyboard_shortcuts.home": "Otevřít domovskou časovou osu",
@ -350,11 +365,13 @@
"lightbox.previous": "Předchozí",
"limited_account_hint.action": "Přesto profil zobrazit",
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
"link_preview.author": "Podle {name}",
"lists.account.add": "Přidat do seznamu",
"lists.account.remove": "Odebrat ze seznamu",
"lists.delete": "Smazat seznam",
"lists.edit": "Upravit seznam",
"lists.edit.submit": "Změnit název",
"lists.exclusive": "Skrýt tyto příspěvky z domovské stránky",
"lists.new.create": "Přidat seznam",
"lists.new.title_placeholder": "Název nového seznamu",
"lists.replies_policy.followed": "Sledovaným uživatelům",
@ -371,6 +388,7 @@
"mute_modal.hide_notifications": "Skrýt oznámení od tohoto uživatele?",
"mute_modal.indefinite": "Neomezeně",
"navigation_bar.about": "O aplikaci",
"navigation_bar.advanced_interface": "Otevřít pokročilé webové rozhraní",
"navigation_bar.blocks": "Blokovaní uživatelé",
"navigation_bar.bookmarks": "Záložky",
"navigation_bar.community_timeline": "Místní časová osa",
@ -380,6 +398,7 @@
"navigation_bar.domain_blocks": "Blokované domény",
"navigation_bar.edit_profile": "Upravit profil",
"navigation_bar.explore": "Prozkoumat",
"navigation_bar.favourites": "Oblíbené",
"navigation_bar.filters": "Skrytá slova",
"navigation_bar.follow_requests": "Žádosti o sledování",
"navigation_bar.followed_tags": "Sledované hashtagy",
@ -396,6 +415,7 @@
"not_signed_in_indicator.not_signed_in": "Pro přístup k tomuto zdroji se musíte přihlásit.",
"notification.admin.report": "Uživatel {name} nahlásil {target}",
"notification.admin.sign_up": "Uživatel {name} se zaregistroval",
"notification.favourite": "Uživatel {name} si oblíbil váš příspěvek",
"notification.follow": "Uživatel {name} vás začal sledovat",
"notification.follow_request": "Uživatel {name} požádal o povolení vás sledovat",
"notification.mention": "Uživatel {name} vás zmínil",
@ -409,6 +429,7 @@
"notifications.column_settings.admin.report": "Nová hlášení:",
"notifications.column_settings.admin.sign_up": "Nové registrace:",
"notifications.column_settings.alert": "Oznámení na počítači",
"notifications.column_settings.favourite": "Oblíbené:",
"notifications.column_settings.filter_bar.advanced": "Zobrazit všechny kategorie",
"notifications.column_settings.filter_bar.category": "Panel rychlého filtrování",
"notifications.column_settings.filter_bar.show_bar": "Zobrazit panel filtrů",
@ -426,6 +447,7 @@
"notifications.column_settings.update": "Úpravy:",
"notifications.filter.all": "Vše",
"notifications.filter.boosts": "Boosty",
"notifications.filter.favourites": "Oblíbené",
"notifications.filter.follows": "Sledování",
"notifications.filter.mentions": "Zmínky",
"notifications.filter.polls": "Výsledky anket",
@ -448,10 +470,12 @@
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Populární na Mastodonu",
"onboarding.share.lead": "Dejte lidem vědět, jak vás mohou najít na Mastodonu!",
"onboarding.share.message": "Jsem {username} na #Mastodonu! Pojď mě sledovat na {url}",
"onboarding.share.next_steps": "Možné další kroky:",
"onboarding.share.title": "Sdílejte svůj profil",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "Dokázali jste to!",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Řekněte světu Ahoj.",
@ -460,11 +484,16 @@
"onboarding.steps.setup_profile.title": "Přizpůsobit svůj profil",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Sdílejte svůj profil",
"onboarding.tips.2fa": "<strong>Víte, že?</strong> Svůj účet můžete zabezpečit nastavením dvoufaktorového ověřování v nastavení účtu. Funguje s jakoukoli TOTP aplikací podle vašeho výběru, telefonní číslo není nutné!",
"onboarding.tips.accounts_from_other_servers": "<strong>Víte, že?</strong> Protože je Mastodon decentralizovaný, některé profily, na které narazíte, budou hostovány na jiných serverech, než je ten váš. A přesto s nimi můžete bezproblémově komunikovat! Jejich server se nachází v druhé polovině uživatelského jména!",
"onboarding.tips.migration": "<strong>Víte, že?</strong> Pokud máte pocit, že {domain} pro vás v budoucnu není vhodnou volbou, můžete se přesunout na jiný Mastodon server, aniž byste přišli o své sledující. Můžete dokonce hostovat svůj vlastní server!",
"onboarding.tips.verification": "<strong>Víte, že?</strong> Svůj účet můžete ověřit tak, že na své webové stránky umístíte odkaz na váš Mastodon profil a odkaz na stránku přidáte do svého profilu. Nejsou k tomu potřeba žádné poplatky ani dokumenty!",
"password_confirmation.exceeds_maxlength": "Potvrzení hesla překračuje maximální délku hesla",
"password_confirmation.mismatching": "Zadaná hesla se neshodují",
"picture_in_picture.restore": "Vrátit zpět",
"poll.closed": "Uzavřeno",
"poll.refresh": "Obnovit",
"poll.reveal": "Zobrazit výsledky",
"poll.total_people": "{count, plural, one {# člověk} few {# lidé} many {# lidí} other {# lidí}}",
"poll.total_votes": "{count, plural, one {# hlas} few {# hlasy} many {# hlasů} other {# hlasů}}",
"poll.vote": "Hlasovat",
@ -517,6 +546,8 @@
"report.placeholder": "Další komentáře",
"report.reasons.dislike": "Nelíbí se mi to",
"report.reasons.dislike_description": "Není to něco, co chcete vidět",
"report.reasons.legal": "Je to nezákonné",
"report.reasons.legal_description": "Domníváte se, že to porušuje zákony vaší země nebo země serveru",
"report.reasons.other": "Je to něco jiného",
"report.reasons.other_description": "Problém neodpovídá ostatním kategoriím",
"report.reasons.spam": "Je to spam",
@ -536,6 +567,7 @@
"report.unfollow": "Přestat sledovat @{name}",
"report.unfollow_explanation": "Tento účet sledujete. Abyste už neviděli jeho příspěvky ve své domovské časové ose, přestaňte jej sledovat.",
"report_notification.attached_statuses": "{count, plural, one {{count} připojený příspěvek} few {{count} připojené příspěvky} many {{count} připojených příspěvků} other {{count} připojených příspěvků}}",
"report_notification.categories.legal": "Zákonné",
"report_notification.categories.other": "Ostatní",
"report_notification.categories.spam": "Spam",
"report_notification.categories.violation": "Porušení pravidla",
@ -566,6 +598,8 @@
"server_banner.server_stats": "Statistiky serveru:",
"sign_in_banner.create_account": "Vytvořit účet",
"sign_in_banner.sign_in": "Přihlásit se",
"sign_in_banner.sso_redirect": "Přihlášení nebo Registrace",
"sign_in_banner.text": "Přihlaste se pro sledování profilů nebo hashtagů, oblíbení, sdílení a odpovídání na příspěvky. Svůj účet můžete také používat k interagování i na jiném serveru.",
"status.admin_account": "Otevřít moderátorské rozhraní pro @{name}",
"status.admin_domain": "Otevřít moderátorské rozhraní pro {domain}",
"status.admin_status": "Otevřít tento příspěvek v moderátorském rozhraní",
@ -582,6 +616,7 @@
"status.edited": "Upraveno {date}",
"status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}",
"status.embed": "Vložit na web",
"status.favourite": "Oblíbit",
"status.filter": "Filtrovat tento příspěvek",
"status.filtered": "Filtrováno",
"status.hide": "Skrýt příspěvek",

@ -191,7 +191,6 @@
"conversation.open": "Gweld sgwrs",
"conversation.with": "Gyda {names}",
"copypaste.copied": "Wedi ei gopïo",
"copypaste.copy": "Copïo",
"copypaste.copy_to_clipboard": "Copïo i'r clipfwrdd",
"directory.federated": "O'r ffedysawd cyfan",
"directory.local": "O {domain} yn unig",
@ -299,22 +298,25 @@
"hashtag.follow": "Dilyn hashnod",
"hashtag.unfollow": "Dad-ddilyn hashnod",
"home.actions.go_to_explore": "Gweld beth sy'n tueddu",
"home.actions.go_to_suggestions": "Ffeindio i bobl i'w dilyn",
"home.actions.go_to_suggestions": "Ffeindio pobl i'w dilyn",
"home.column_settings.basic": "Syml",
"home.column_settings.show_reblogs": "Dangos hybiau",
"home.column_settings.show_replies": "Dangos atebion",
"home.explore_prompt.body": "Bydd eich llif cartref yn cynnwys cymysgedd o bostiadau o'r hashnodau rydych chi wedi dewis eu dilyn, y bobl rydych chi wedi dewis eu dilyn, a'r postiadau maen nhw'n rhoi hwb iddyn nhw. Mae'n edrych yn eithaf tawel ar hyn o bryd, felly beth am:",
"home.explore_prompt.title": "Dyma'ch cartref o feewnn Mastodon.",
"home.explore_prompt.body": "Bydd eich llif cartref yn cynnwys cymysgedd o bostiadau o'r hashnodau rydych chi wedi dewis eu dilyn, y bobl rydych chi wedi dewis eu dilyn, a'r postiadau maen nhw'n rhoi hwb iddyn nhw. Os yw hynny'n teimlo'n rhy dawel, efallai y byddwch am:",
"home.explore_prompt.title": "Dyma'ch cartref o fewn Mastodon.",
"home.hide_announcements": "Cuddio cyhoeddiadau",
"home.show_announcements": "Dangos cyhoeddiadau",
"interaction_modal.description.favourite": "Gyda chyfrif ar Mastodon, gallwch chi hoffi'r postiad hwn er mwyn roi gwybod i'r awdur eich bod chi'n ei werthfawrogi ac yn ei gadw ar gyfer nes ymlaen.",
"interaction_modal.description.follow": "Gyda chyfrif ar Mastodon, gallwch ddilyn {name} i dderbyn eu postiadau yn eich llif cartref.",
"interaction_modal.description.reblog": "Gyda chyfrif ar Mastodon, gallwch hybu'r postiad hwn i'w rannu â'ch dilynwyr.",
"interaction_modal.description.reply": "Gyda chyfrif ar Mastodon, gallwch ymateb i'r postiad hwn.",
"interaction_modal.login.action": "Ewch â fi adref",
"interaction_modal.login.prompt": "Parth eich gweinydd cartref, e.e. mastodon.social",
"interaction_modal.no_account_yet": "Dim ar Mastodon?",
"interaction_modal.on_another_server": "Ar weinydd gwahanol",
"interaction_modal.on_this_server": "Ar y gweinydd hwn",
"interaction_modal.other_server_instructions": "Copïwch a gludwch yr URL hwn i faes chwilio eich hoff ap Mastodon neu ryngwyneb gwe eich gweinydd Mastodon.",
"interaction_modal.preamble": "Gan fod Mastodon wedi'i ddatganoli, gallwch ddefnyddio'ch cyfrif presennol a gynhelir gan weinydd Mastodon arall neu blatfform cydnaws os nad oes gennych gyfrif ar yr un hwn.",
"interaction_modal.sign_in": "Nid ydych wedi mewngofnodi i'r gweinydd hwn. Ble mae eich cyfrif yn cael ei gynnal?",
"interaction_modal.sign_in_hint": "Awgrym: Dyna'r wefan lle gwnaethoch gofrestru. Os nad ydych yn cofio, edrychwch am yr e-bost croeso yn eich blwch derbyn. Gallwch hefyd nodi eich enw defnyddiwr llawn! (e.e. @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Hoffi postiad {name}",
"interaction_modal.title.follow": "Dilyn {name}",
"interaction_modal.title.reblog": "Hybu postiad {name}",
@ -363,6 +365,7 @@
"lightbox.previous": "Blaenorol",
"limited_account_hint.action": "Dangos y proffil beth bynnag",
"limited_account_hint.title": "Mae'r proffil hwn wedi cael ei guddio gan gymedrolwyr {domain}.",
"link_preview.author": "Gan {name}",
"lists.account.add": "Ychwanegu at restr",
"lists.account.remove": "Tynnu o'r rhestr",
"lists.delete": "Dileu rhestr",
@ -387,7 +390,7 @@
"navigation_bar.about": "Ynghylch",
"navigation_bar.advanced_interface": "Agor mewn rhyngwyneb gwe uwch",
"navigation_bar.blocks": "Defnyddwyr wedi eu blocio",
"navigation_bar.bookmarks": "Llyfrnodau",
"navigation_bar.bookmarks": "Nodau Tudalen",
"navigation_bar.community_timeline": "Ffrwd leol",
"navigation_bar.compose": "Cyfansoddi post newydd",
"navigation_bar.direct": "Crybwylliadau preifat",
@ -456,8 +459,8 @@
"notifications.permission_denied_alert": "Nid oes modd galluogi hysbysiadau bwrdd gwaith, gan fod caniatâd porwr wedi'i wrthod o'r blaen",
"notifications.permission_required": "Nid oes hysbysiadau bwrdd gwaith ar gael oherwydd na roddwyd y caniatâd gofynnol.",
"notifications_permission_banner.enable": "Galluogi hysbysiadau bwrdd gwaith",
"notifications_permission_banner.how_to_control": "I dderbyn hysbysiadau pan nad yw Mastodon ar agor, galluogwch hysbysiadau bwrdd gwaith. Gallwch reoli'n union pa fathau o ryngweithiadau sy'n cynhyrchu hysbysiadau bwrdd gwaith trwy'r botwm {icon} uchod unwaith y byddant wedi'u galluogi.",
"notifications_permission_banner.title": "Peidiwch colli dim",
"notifications_permission_banner.how_to_control": "I dderbyn hysbysiadau pan nad yw Mastodon ar agor, galluogwch hysbysiadau bwrdd gwaith. Gallwch reoli'n union pa fathau o ryngweithiadau sy'n cynhyrchu hysbysiadau bwrdd gwaith trwy'r botwm {icon} uchod unwaith y byddan nhw wedi'u galluogi.",
"notifications_permission_banner.title": "Peidiwch â cholli dim",
"onboarding.action.back": "Ewch â fi yn ôl",
"onboarding.actions.back": "Ewch â fi yn ôl",
"onboarding.actions.go_to_explore": "Gweld beth yw'r tuedd",
@ -501,7 +504,7 @@
"privacy.change": "Addasu preifatrwdd y post",
"privacy.direct.long": "Dim ond yn weladwy i ddefnyddwyr a grybwyllwyd",
"privacy.direct.short": "Dim ond pobl sy wedi'u crybwyll",
"privacy.private.long": "Dim ond pobl sy'n ddilynwyrl",
"privacy.private.long": "Dim ond pobl sy'n ddilynwyr",
"privacy.private.short": "Dilynwyr yn unig",
"privacy.public.long": "Gweladwy i bawb",
"privacy.public.short": "Cyhoeddus",
@ -510,7 +513,7 @@
"privacy_policy.last_updated": "Diweddarwyd ddiwethaf ar {date}",
"privacy_policy.title": "Polisi Preifatrwydd",
"refresh": "Adnewyddu",
"regeneration_indicator.label": "Llwytho…",
"regeneration_indicator.label": "Yn llwytho…",
"regeneration_indicator.sublabel": "Mae eich ffrwd cartref yn cael ei baratoi!",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# diwrnod} other {# diwrnod}} yn ôl",
@ -590,11 +593,12 @@
"server_banner.about_active_users": "Pobl sy'n defnyddio'r gweinydd hwn yn ystod y 30 diwrnod diwethaf (Defnyddwyr Gweithredol Misol)",
"server_banner.active_users": "defnyddwyr gweithredol",
"server_banner.administered_by": "Gweinyddir gan:",
"server_banner.introduction": "Mae {domain} yn rhan o'r rhwydwaith cymdeithasol datganoledig a bwerir gan {mastodon}.",
"server_banner.introduction": "Mae {domain} yn rhan o'r rhwydwaith cymdeithasol datganoledig sy'n cael ei bweru gan {mastodon}.",
"server_banner.learn_more": "Dysgu mwy",
"server_banner.server_stats": "Ystadegau'r gweinydd:",
"sign_in_banner.create_account": "Creu cyfrif",
"sign_in_banner.sign_in": "Mewngofnodi",
"sign_in_banner.sso_redirect": "Mewngofnodi neu Gofrestru",
"sign_in_banner.text": "Mewngofnodwch i ddilyn proffiliau neu hashnodau, ffefrynnau, rhannu ac ymateb i bostiadau. Gallwch hefyd ryngweithio o'ch cyfrif ar weinyddion gwahanol.",
"status.admin_account": "Agor rhyngwyneb cymedroli ar gyfer @{name}",
"status.admin_domain": "Agor rhyngwyneb cymedroli {domain}",
@ -635,7 +639,7 @@
"status.reblogged_by": "Hybodd {name}",
"status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
"status.redraft": "Dileu ac ailddrafftio",
"status.remove_bookmark": "Dileu llyfrnod",
"status.remove_bookmark": "Tynnu nod tudalen",
"status.replied_to": "Wedi ateb {name}",
"status.reply": "Ateb",
"status.replyAll": "Ateb i edefyn",
@ -678,7 +682,7 @@
"units.short.thousand": "{count}mil",
"upload_area.title": "Llusgwch a gollwng i lwytho",
"upload_button.label": "Ychwanegwch gyfryngau (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Wedi mynd heibio'r uchafswm terfyn uwchlwytho.",
"upload_error.limit": "Wedi pasio'r uchafswm llwytho.",
"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",

@ -191,7 +191,6 @@
"conversation.open": "Vis samtale",
"conversation.with": "Med {names}",
"copypaste.copied": "Kopieret",
"copypaste.copy": "Kopiér",
"copypaste.copy_to_clipboard": "Kopiér til udklipsholder",
"directory.federated": "Fra kendt fedivers",
"directory.local": "Kun fra {domain}",
@ -296,6 +295,9 @@
"hashtag.column_settings.tag_mode.any": "Nogle af disse",
"hashtag.column_settings.tag_mode.none": "Ingen af disse",
"hashtag.column_settings.tag_toggle": "Inkludér ekstra tags for denne kolonne",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} deltager} other {{counter} deltagere}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}} i dag",
"hashtag.follow": "Følg hashtag",
"hashtag.unfollow": "Stop med at følge hashtag",
"home.actions.go_to_explore": "Se, hvad som trender",
@ -303,7 +305,7 @@
"home.column_settings.basic": "Grundlæggende",
"home.column_settings.show_reblogs": "Vis boosts",
"home.column_settings.show_replies": "Vis svar",
"home.explore_prompt.body": "Dit hjemmefeed vil have en blanding af indlæg fra de hashtags, du har valgt at følge, de personer, du har valgt at følge, og de indlæg, de booster. Her virker temmelig stille lige nu, så hvad med at prøve:",
"home.explore_prompt.body": "Hjemmefeedet vil indeholde en blanding af indlæg fra de hashtags og personer, du følger samt de indlæg, de booster. Føles synes for stille, kan du prøve:",
"home.explore_prompt.title": "Dette er din hjemmebase i Mastodon.",
"home.hide_announcements": "Skjul bekendtgørelser",
"home.show_announcements": "Vis bekendtgørelser",
@ -311,10 +313,13 @@
"interaction_modal.description.follow": "Med en konto på Mastodon kan du følge {name} for at modtage vedkommendes indlæg i dit hjemmefeed.",
"interaction_modal.description.reblog": "Med en konto på Mastodon kan dette indlæg fremhæves så det deles med egne følgere.",
"interaction_modal.description.reply": "Med en konto på Mastodon kan dette indlæg besvares.",
"interaction_modal.login.action": "Gå til hjemmeserver",
"interaction_modal.login.prompt": "Hjemmeserverdomænet, f.eks. mastodon.social",
"interaction_modal.no_account_yet": "Ikke på Mastodon?",
"interaction_modal.on_another_server": "På en anden server",
"interaction_modal.on_this_server": "På denne server",
"interaction_modal.other_server_instructions": "Kopiér og indsæt denne URL i søgefeltet på den foretrukne Mastodon-app eller Mastodon-serverens webgrænseflade.",
"interaction_modal.preamble": "Da Mastodon er decentraliseret, kan man bruge sin eksisterende konto hostet af en anden Mastodon-server eller kompatibel platform, såfremt man ikke har en konto på denne.",
"interaction_modal.sign_in": "Du er ikke logget ind på denne server. Hvor hostes din konto?",
"interaction_modal.sign_in_hint": "Tip: Det er webstedet, hvor du tilmeldte dig. Har du glemt det, så kig efter velkomstmailen i indbakken. Du kan også angive dit fulde brugernavn! (f.eks. @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Gør {name}s indlæg til favorit",
"interaction_modal.title.follow": "Følg {name}",
"interaction_modal.title.reblog": "Boost {name}s indlæg",
@ -596,6 +601,7 @@
"server_banner.server_stats": "Serverstatstik:",
"sign_in_banner.create_account": "Opret konto",
"sign_in_banner.sign_in": "Log ind",
"sign_in_banner.sso_redirect": "Log ind eller Tilmeld",
"sign_in_banner.text": "Log ind for at følge profiler eller hashtags, markere som favorit, dele og besvare indlæg eller interagere fra din konto på en anden server.",
"status.admin_account": "Åbn modereringsbrugerflade for @{name}",
"status.admin_domain": "Åbn modereringsbrugerflade for {domain}",

@ -150,7 +150,7 @@
"compose_form.poll.switch_to_multiple": "Mehrfachauswahl erlauben",
"compose_form.poll.switch_to_single": "Nur Einzelauswahl erlauben",
"compose_form.publish": "Veröffentlichen",
"compose_form.publish_form": "Veröffentlichen",
"compose_form.publish_form": "Neuer Beitrag",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Änderungen speichern",
"compose_form.sensitive.hide": "{count, plural, one {Mit einer Inhaltswarnung versehen} other {Mit einer Inhaltswarnung versehen}}",
@ -191,7 +191,6 @@
"conversation.open": "Unterhaltung anzeigen",
"conversation.with": "Mit {names}",
"copypaste.copied": "Kopiert",
"copypaste.copy": "Kopieren",
"copypaste.copy_to_clipboard": "In die Zwischenablage kopieren",
"directory.federated": "Aus bekanntem Fediverse",
"directory.local": "Nur von der Domain {domain}",
@ -202,7 +201,7 @@
"dismissable_banner.community_timeline": "Das sind die neuesten öffentlichen Beiträge von Profilen, deren Konten von {domain} verwaltet werden.",
"dismissable_banner.dismiss": "Ablehnen",
"dismissable_banner.explore_links": "Diese Nachrichten werden heute am häufigsten im sozialen Netzwerk geteilt. Neuere Nachrichten, die von vielen verschiedenen Profilen veröffentlicht wurden, werden höher eingestuft.",
"dismissable_banner.explore_statuses": "Diese Beiträge stammen aus dem gesamten sozialen Netz und gewinnen derzeit an Reichweite. Neuere Beiträge, die häufiger geteilt und favorisiert wurden, werden höher eingestuft.",
"dismissable_banner.explore_statuses": "Diese Beiträge stammen aus dem gesamten sozialen Netzwerk und gewinnen derzeit an Reichweite. Neuere Beiträge, die häufiger geteilt und favorisiert wurden, werden höher eingestuft.",
"dismissable_banner.explore_tags": "Das sind Hashtags, die derzeit an Reichweite gewinnen. Hashtags, die von vielen verschiedenen Profilen verwendet werden, werden höher eingestuft.",
"dismissable_banner.public_timeline": "Das sind die neuesten öffentlichen Beiträge von Profilen im sozialen Netzwerk, denen Leute auf {domain} folgen.",
"embed.instructions": "Du kannst diesen Beitrag außerhalb des Fediverse (z. B. auf deiner Website) einbetten, indem du diesen iFrame-Code einfügst.",
@ -296,6 +295,9 @@
"hashtag.column_settings.tag_mode.any": "Eines von diesen",
"hashtag.column_settings.tag_mode.none": "Keines von diesen",
"hashtag.column_settings.tag_toggle": "Zusätzliche Hashtags dieser Spalte hinzufügen",
"hashtag.counter_by_accounts": "{count, plural, one{{counter} Beteiligte*r} other{{counter} Beteiligte}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}} heute",
"hashtag.follow": "Hashtag folgen",
"hashtag.unfollow": "Hashtag entfolgen",
"home.actions.go_to_explore": "Trends ansehen",
@ -303,7 +305,7 @@
"home.column_settings.basic": "Einfach",
"home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
"home.column_settings.show_replies": "Antworten anzeigen",
"home.explore_prompt.body": "Deine Startseite wird eine Mischung aus Beiträgen mit gefolgten Hashtags und den Profilen, denen du folgst sowie den Beiträgen, die sie teilen, enthalten. Aktuell ist es noch etwas still. Wie wäre es mit:",
"home.explore_prompt.body": "Deine Startseite wird eine Mischung aus Beiträgen mit Hashtags und den Profilen, denen du folgst sowie den Beiträgen, die sie teilen, enthalten. Sollte es sich zu still anfühlen:",
"home.explore_prompt.title": "Das ist dein Zuhause bei Mastodon.",
"home.hide_announcements": "Ankündigungen ausblenden",
"home.show_announcements": "Ankündigungen anzeigen",
@ -311,10 +313,13 @@
"interaction_modal.description.follow": "Mit einem Mastodon-Konto kannst du {name} folgen, um die Beiträge auf deiner Startseite zu sehen.",
"interaction_modal.description.reblog": "Mit einem Mastodon-Konto kannst du die Reichweite dieses Beitrags erhöhen, indem du ihn mit deinen Followern teilst.",
"interaction_modal.description.reply": "Mit einem Mastodon-Konto kannst du auf diesen Beitrag antworten.",
"interaction_modal.login.action": "Zurück zur Startseite",
"interaction_modal.login.prompt": "Adresse deines Servers, z. B. mastodon.social",
"interaction_modal.no_account_yet": "Nicht auf Mastodon?",
"interaction_modal.on_another_server": "Auf einem anderen Server",
"interaction_modal.on_this_server": "Auf diesem Server",
"interaction_modal.other_server_instructions": "Kopiere diese URL und füge sie in das Suchfeld deiner bevorzugten Mastodon-App oder in das Webinterface deines Mastodon-Servers ein.",
"interaction_modal.preamble": "Da Mastodon dezentralisiert ist, kannst du dein bestehendes Konto auf einem anderen Mastodon-Server oder einer kompatiblen Plattform nutzen, wenn du kein Konto auf dieser Plattform hast.",
"interaction_modal.sign_in": "Du bist auf diesem Server nicht angemeldet. Auf welchem Server wird dein Konto verwaltet?",
"interaction_modal.sign_in_hint": "Hinweis: Hierbei handelt es sich um die Website, auf der du dich registriert hast. Wenn du dich nicht mehr daran erinnerst, dann kannst du sie in der Willkommens-E-Mail nachsehen. Du kannst auch deinen vollständigen Profilnamen eingeben! (z. B. @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Beitrag von {name} favorisieren",
"interaction_modal.title.follow": "Folge {name}",
"interaction_modal.title.reblog": "Beitrag von {name} teilen",
@ -471,7 +476,7 @@
"onboarding.share.message": "Ich bin {username} auf #Mastodon! Folge mir auf {url}",
"onboarding.share.next_steps": "Mögliche nächste Schritte:",
"onboarding.share.title": "Teile dein Profil",
"onboarding.start.lead": "Du bist nun ein Teil von Mastodon eine einzigartige, dezentralisierte Social Media-Plattform, bei der du und kein Algorithmus deine eigene Erfahrung gestaltest. Fangen wir an, diese neue soziale Dimension zu erkunden:",
"onboarding.start.lead": "Du bist nun ein Teil von Mastodon eine einzigartige, dezentralisierte Social-Media-Plattform, bei der du und kein Algorithmus deine eigene Erfahrung gestaltest. Fangen wir an, diese neue soziale Dimension zu erkunden:",
"onboarding.start.skip": "Du benötigst keine Hilfe für den Einstieg?",
"onboarding.start.title": "Du hast es geschafft!",
"onboarding.steps.follow_people.body": "Interessanten Profilen zu folgen ist das, was Mastodon ausmacht.",
@ -506,7 +511,7 @@
"privacy.private.short": "Nur Follower",
"privacy.public.long": "Für alle sichtbar",
"privacy.public.short": "Öffentlich",
"privacy.unlisted.long": "Sichtbar für alle, aber nicht über Suchfunktion",
"privacy.unlisted.long": "Für alle sichtbar, aber nicht über die Suche zu finden",
"privacy.unlisted.short": "Nicht gelistet",
"privacy_policy.last_updated": "Stand: {date}",
"privacy_policy.title": "Datenschutzerklärung",
@ -526,7 +531,7 @@
"relative_time.today": "heute",
"reply_indicator.cancel": "Abbrechen",
"report.block": "Blockieren",
"report.block_explanation": "Dir wird es nicht länger möglich sein, die Beiträge dieses Konto zu sehen. Das blockierte Profil wird nicht mehr in der Lage sein, deine Beiträge zu sehen oder dir zu folgen. Die Person hinter dem Konto wird mitbekommen, dass du ihr Konto blockiert hast.",
"report.block_explanation": "Du wirst keine Beiträge mehr von diesem Konto sehen. Das blockierte Konto wird deine Beiträge nicht mehr sehen oder dir folgen können. Die Person könnte mitbekommen, dass du sie blockiert hast.",
"report.categories.other": "Andere",
"report.categories.spam": "Spam",
"report.categories.violation": "Der Inhalt verletzt eine oder mehrere Serverregeln",
@ -539,13 +544,13 @@
"report.forward": "Meldung zusätzlich an {target} weiterleiten",
"report.forward_hint": "Dieses Konto gehört zu einem anderen Server. Soll eine anonymisierte Kopie der Meldung auch dorthin gesendet werden?",
"report.mute": "Stummschalten",
"report.mute_explanation": "Du wirst die Beiträge vom Konto nicht mehr sehen. Das Konto kann dir immer noch folgen, und die Person hinter dem Konto wird deine Beiträge sehen können und nicht wissen, dass du sie stummgeschaltet hast.",
"report.mute_explanation": "Du wirst keine Beiträge mehr von diesem Konto sehen. Das stummgeschaltete Konto wird dir weiterhin folgen und deine Beiträge sehen können. Die Person wird nicht mitbekommen, dass du sie stummgeschaltet hast.",
"report.next": "Weiter",
"report.placeholder": "Ergänzende Hinweise",
"report.reasons.dislike": "Das gefällt mir nicht",
"report.reasons.dislike_description": "Das ist etwas, das du nicht sehen möchtest",
"report.reasons.legal": "Das ist illegal",
"report.reasons.legal_description": "Du bist davon überzeugt, dass es gegen die Gesetze deines Landes oder des Landes des Servers verstößt",
"report.reasons.legal_description": "Du glaubst, dass es gegen die Gesetze deines Landes oder des Landes des Servers verstößt",
"report.reasons.other": "Es ist etwas anderes",
"report.reasons.other_description": "Der Vorfall passt zu keiner dieser Kategorien",
"report.reasons.spam": "Das ist Spam",
@ -555,8 +560,8 @@
"report.rules.subtitle": "Wähle alle zutreffenden Inhalte aus",
"report.rules.title": "Welche Regeln werden verletzt?",
"report.statuses.subtitle": "Wähle alle zutreffenden Inhalte aus",
"report.statuses.title": "Gibt es Beiträge, die diesen Bericht untermauern?",
"report.submit": "Absenden",
"report.statuses.title": "Gibt es Beiträge, die diese Meldung bekräftigen?",
"report.submit": "Senden",
"report.target": "{target} melden",
"report.thanks.take_action": "Das sind deine Möglichkeiten zu bestimmen, was du auf Mastodon sehen möchtest:",
"report.thanks.take_action_actionable": "Während wir den Vorfall überprüfen, kannst du gegen @{name} weitere Maßnahmen ergreifen:",
@ -591,11 +596,12 @@
"server_banner.about_active_users": "Personen, die diesen Server in den vergangenen 30 Tagen verwendet haben (monatlich aktive Nutzer*innen)",
"server_banner.active_users": "aktive Profile",
"server_banner.administered_by": "Verwaltet von:",
"server_banner.introduction": "{domain} ist ein Teil des dezentralisierten sozialen Netzwerks, angetrieben von {mastodon}.",
"server_banner.introduction": "{domain} ist Teil eines dezentralisierten sozialen Netzwerks, angetrieben von {mastodon}.",
"server_banner.learn_more": "Mehr erfahren",
"server_banner.server_stats": "Serverstatistiken:",
"sign_in_banner.create_account": "Konto erstellen",
"sign_in_banner.sign_in": "Anmelden",
"sign_in_banner.sso_redirect": "Anmelden oder registrieren",
"sign_in_banner.text": "Melde dich an, um Profilen oder Hashtags zu folgen, Beiträge zu favorisieren, zu teilen und auf sie zu antworten. Du kannst auch von deinem Konto aus auf einem anderen Server interagieren.",
"status.admin_account": "@{name} moderieren",
"status.admin_domain": "{domain} moderieren",

@ -177,7 +177,6 @@
"conversation.open": "Προβολή συνομιλίας",
"conversation.with": "Με {names}",
"copypaste.copied": "Αντιγράφηκε",
"copypaste.copy": "Αντιγραφή",
"copypaste.copy_to_clipboard": "Αντιγραφή στο πρόχειρο",
"directory.federated": "Από το γνωστό fediverse",
"directory.local": "Μόνο από {domain}",
@ -287,7 +286,6 @@
"interaction_modal.description.reply": "Με ένα λογαριασμό Mastodon, μπορείς να απαντήσεις σε αυτή την ανάρτηση.",
"interaction_modal.on_another_server": "Σε διαφορετικό διακομιστή",
"interaction_modal.on_this_server": "Σε αυτόν τον διακομιστή",
"interaction_modal.preamble": "Δεδομένου ότι το Mastodon είναι αποκεντρωμένο, μπορείς να χρησιμοποιείς τον υπάρχοντα λογαριασμό σου που φιλοξενείται σε άλλον διακομιστή του Mastodon ή σε συμβατή πλατφόρμα, αν δεν έχετε λογαριασμό σε αυτόν.",
"interaction_modal.title.follow": "Ακολούθησε {name}",
"interaction_modal.title.reblog": "Ενίσχυσε την ανάρτηση του {name}",
"interaction_modal.title.reply": "Απάντηση στην ανάρτηση του {name}",

@ -13,14 +13,14 @@
"about.rules": "Server rules",
"account.account_note_header": "Note",
"account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot",
"account.badges.bot": "Automated",
"account.badges.group": "Group",
"account.block": "Block @{name}",
"account.block_domain": "Unblock domain {domain}",
"account.block_domain": "Block domain {domain}",
"account.block_short": "Block",
"account.blocked": "Blocked",
"account.browse_more_on_origin_server": "Browse more on the original profile",
"account.cancel_follow_request": "Withdraw follow request",
"account.cancel_follow_request": "Cancel follow",
"account.direct": "Privately mention @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Domain blocked",
@ -150,7 +150,7 @@
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Publish",
"compose_form.publish_form": "Publish",
"compose_form.publish_form": "New post",
"compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
@ -191,7 +191,6 @@
"conversation.open": "View conversation",
"conversation.with": "With {names}",
"copypaste.copied": "Copied",
"copypaste.copy": "Copy",
"copypaste.copy_to_clipboard": "Copy to clipboard",
"directory.federated": "From known fediverse",
"directory.local": "From {domain} only",
@ -236,7 +235,7 @@
"empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.",
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.",
"empty_column.hashtag": "There is nothing in this hashtag yet.",
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}",
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up.",
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
"empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.",
"empty_column.mutes": "You haven't muted any users yet.",
@ -296,6 +295,7 @@
"hashtag.column_settings.tag_mode.any": "Any of these",
"hashtag.column_settings.tag_mode.none": "None of these",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} Following} other {{counter} Following}}",
"hashtag.follow": "Follow hashtag",
"hashtag.unfollow": "Unfollow hashtag",
"home.actions.go_to_explore": "See what's trending",
@ -303,7 +303,7 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:",
"home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:",
"home.explore_prompt.title": "This is your home base within Mastodon.",
"home.hide_announcements": "Hide announcements",
"home.show_announcements": "Show announcements",
@ -311,10 +311,13 @@
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.",
"interaction_modal.description.reblog": "With an account on Mastodon, you can boost this post to share it with your own followers.",
"interaction_modal.description.reply": "With an account on Mastodon, you can respond to this post.",
"interaction_modal.login.action": "Take me home",
"interaction_modal.login.prompt": "Domain of your home server, e.g. mastodon.social",
"interaction_modal.no_account_yet": "Not on Mastodon?",
"interaction_modal.on_another_server": "On a different server",
"interaction_modal.on_this_server": "On this server",
"interaction_modal.other_server_instructions": "Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.",
"interaction_modal.preamble": "Since Mastodon is decentralised, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one.",
"interaction_modal.sign_in": "You are not logged in to this server. Where is your account hosted?",
"interaction_modal.sign_in_hint": "Tip: That's the website where you signed up. If you don't remember, look for the welcome e-mail in your inbox. You can also enter your full username! (e.g. @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Favourite {name}'s post",
"interaction_modal.title.follow": "Follow {name}",
"interaction_modal.title.reblog": "Boost {name}'s post",
@ -335,14 +338,14 @@
"keyboard_shortcuts.favourites": "Open favourites list",
"keyboard_shortcuts.federated": "to open federated timeline",
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "to open home timeline",
"keyboard_shortcuts.home": "Open home timeline",
"keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.local": "to open local timeline",
"keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.muted": "to open muted users list",
"keyboard_shortcuts.my_profile": "to open your profile",
"keyboard_shortcuts.notifications": "to open notifications column",
"keyboard_shortcuts.notifications": "Open notifications column",
"keyboard_shortcuts.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned posts list",
"keyboard_shortcuts.profile": "to open author's profile",
@ -352,10 +355,10 @@
"keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW",
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.toggle_sensitivity": "Show/hide media",
"keyboard_shortcuts.toot": "to start a brand new post",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list",
"keyboard_shortcuts.up": "Move up in the list",
"lightbox.close": "Close",
"lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box",
@ -393,7 +396,7 @@
"navigation_bar.compose": "Compose new post",
"navigation_bar.direct": "Private mentions",
"navigation_bar.discover": "Discover",
"navigation_bar.domain_blocks": "Hidden domains",
"navigation_bar.domain_blocks": "Blocked domains",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.explore": "Explore",
"navigation_bar.favourites": "Favourites",
@ -462,26 +465,26 @@
"onboarding.action.back": "Take me back",
"onboarding.actions.back": "Take me back",
"onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.actions.go_to_home": "Take me to my home feed",
"onboarding.compose.template": "Hello #Mastodon!",
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
"onboarding.follows.title": "Popular on Mastodon",
"onboarding.follows.title": "Personalize your home feed",
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
"onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
"onboarding.share.next_steps": "Possible next steps:",
"onboarding.share.title": "Share your profile",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.lead": "You're now part of Mastodon, a unique, decentralized social media platform where you—not an algorithm—curate your own experience. Let's get you started on this new social frontier:",
"onboarding.start.skip": "Don't need help getting started?",
"onboarding.start.title": "You've made it!",
"onboarding.steps.follow_people.body": "Following interesting people is what Mastodon is all about.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the World.",
"onboarding.steps.follow_people.title": "Personalize your home feed",
"onboarding.steps.publish_status.body": "Say hello to the world with text, photos, videos, or polls {emoji}",
"onboarding.steps.publish_status.title": "Make your first post",
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customise your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile",
"onboarding.steps.share_profile.title": "Share your Mastodon profile",
"onboarding.tips.2fa": "<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!",
"onboarding.tips.accounts_from_other_servers": "<strong>Did you know?</strong> Since Mastodon is decentralised, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!",
"onboarding.tips.migration": "<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!",
@ -498,10 +501,10 @@
"poll.voted": "You voted for this answer",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Add a poll",
"privacy.change": "Adjust status privacy",
"poll_button.remove_poll": "Remove poll",
"privacy.change": "Change post privacy",
"privacy.direct.long": "Visible for mentioned users only",
"privacy.direct.short": "Direct",
"privacy.direct.short": "Mentioned people only",
"privacy.private.long": "Visible for followers only",
"privacy.private.short": "Followers-only",
"privacy.public.long": "Visible for all",
@ -541,7 +544,7 @@
"report.mute": "Mute",
"report.mute_explanation": "You will not see their posts. They can still follow you and see your posts and will not know that they are muted.",
"report.next": "Next",
"report.placeholder": "Type or paste additional comments",
"report.placeholder": "Additional comments",
"report.reasons.dislike": "I don't like it",
"report.reasons.dislike_description": "It is not something you want to see",
"report.reasons.legal": "It's illegal",
@ -556,8 +559,8 @@
"report.rules.title": "Which rules are being violated?",
"report.statuses.subtitle": "Select all that apply",
"report.statuses.title": "Are there any posts that back up this report?",
"report.submit": "Submit report",
"report.target": "Report {target}",
"report.submit": "Submit",
"report.target": "Reporting {target}",
"report.thanks.take_action": "Here are your options for controlling what you see on Mastodon:",
"report.thanks.take_action_actionable": "While we review this, you can take action against @{name}:",
"report.thanks.title": "Don't want to see this?",
@ -596,10 +599,11 @@
"server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account",
"sign_in_banner.sign_in": "Sign in",
"sign_in_banner.sso_redirect": "Login or Register",
"sign_in_banner.text": "Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_domain": "Open moderation interface for {domain}",
"status.admin_status": "Open this status in the moderation interface",
"status.admin_status": "Open this post in the moderation interface",
"status.block": "Block @{name}",
"status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost",
@ -627,7 +631,7 @@
"status.more": "More",
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this status",
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.read_more": "Read more",
@ -681,13 +685,13 @@
"upload_button.label": "Add images, a video or an audio file",
"upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people with hearing loss",
"upload_form.description": "Describe for the visually impaired",
"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.description_missing": "No description added",
"upload_form.edit": "Edit",
"upload_form.thumbnail": "Change thumbnail",
"upload_form.undo": "Delete",
"upload_form.video_description": "Describe for people with hearing loss or visual impairment",
"upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",
"upload_modal.analyzing_picture": "Analysing picture…",
"upload_modal.apply": "Apply",
"upload_modal.applying": "Applying…",
@ -700,7 +704,7 @@
"upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Uploading…",
"upload_progress.processing": "Processing…",
"username.taken": "Username is taken - try another. Carlito77",
"username.taken": "That username is taken. Try another",
"video.close": "Close video",
"video.download": "Download file",
"video.exit_fullscreen": "Exit full screen",

@ -295,8 +295,12 @@
"hashtag.column_settings.tag_mode.any": "Any of these",
"hashtag.column_settings.tag_mode.none": "None of these",
"hashtag.column_settings.tag_toggle": "Include additional tags for this column",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participant} other {{counter} participants}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} post} other {{counter} posts}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} post} other {{counter} posts}} today",
"hashtag.follow": "Follow hashtag",
"hashtag.unfollow": "Unfollow hashtag",
"hashtags.and_other": "…and {count, plural, other {# more}}",
"home.actions.go_to_explore": "See what's trending",
"home.actions.go_to_suggestions": "Find people to follow",
"home.column_settings.basic": "Basic",
@ -529,6 +533,7 @@
"reply_indicator.cancel": "Cancel",
"report.block": "Block",
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
"report.categories.legal": "Legal",
"report.categories.other": "Other",
"report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules",
@ -598,6 +603,7 @@
"server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account",
"sign_in_banner.sign_in": "Login",
"sign_in_banner.sso_redirect": "Login or Register",
"sign_in_banner.text": "Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.",
"status.admin_account": "Open moderation interface for @{name}",
"status.admin_domain": "Open moderation interface for {domain}",

@ -189,7 +189,6 @@
"conversation.open": "Vidi konversacion",
"conversation.with": "Kun {names}",
"copypaste.copied": "Kopiita",
"copypaste.copy": "Kopii",
"copypaste.copy_to_clipboard": "Kopii al dosierujo",
"directory.federated": "El konata fediverso",
"directory.local": "Nur de {domain}",
@ -303,7 +302,6 @@
"interaction_modal.description.reply": "Kun konto ĉe Mastodon, vi povos respondi al ĉi tiu mesaĝo.",
"interaction_modal.on_another_server": "En alia servilo",
"interaction_modal.on_this_server": "En ĉi tiu servilo",
"interaction_modal.preamble": "Ĉar Mastodon estas malcentrigita, vi povas uzi jam ekzistantan konton gastigatan de alia Mastodona servilo aŭ kongrua substrato, se vi ne havas konton ĉe tiu ĉi.",
"interaction_modal.title.follow": "Sekvi {name}",
"interaction_modal.title.reblog": "Akceli la afiŝon de {name}",
"interaction_modal.title.reply": "Respondi al la afiŝo de {name}",

@ -191,7 +191,6 @@
"conversation.open": "Ver conversación",
"conversation.with": "Con {names}",
"copypaste.copied": "Copiado",
"copypaste.copy": "Copiar",
"copypaste.copy_to_clipboard": "Copiar al portapapeles",
"directory.federated": "Desde fediverso conocido",
"directory.local": "Sólo de {domain}",
@ -296,6 +295,9 @@
"hashtag.column_settings.tag_mode.any": "Cualquiera de estas",
"hashtag.column_settings.tag_mode.none": "Ninguna de estas",
"hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionales para esta columna",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}} hoy",
"hashtag.follow": "Seguir etiqueta",
"hashtag.unfollow": "Dejar de seguir etiqueta",
"home.actions.go_to_explore": "Mirá qué está en tendencia",
@ -303,7 +305,7 @@
"home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar adhesiones",
"home.column_settings.show_replies": "Mostrar respuestas",
"home.explore_prompt.body": "Tu línea temporal principal tendrá una mezcla de mensajes de etiquetas que hayás decidido seguir, cuentas que hayás seguido y mensajes a los que éstas adhieran. Ahora está muy tranquilo por acá, así que, qué te parece:",
"home.explore_prompt.body": "Tu línea temporal principal tendrá una mezcla de mensajes de etiquetas que hayás decidido seguir, cuentas que hayás seguido y mensajes a los que éstas adhieran. Si está muy tranquilo por acá, quizás quieras:",
"home.explore_prompt.title": "Este es tu inicio en Mastodon.",
"home.hide_announcements": "Ocultar anuncios",
"home.show_announcements": "Mostrar anuncios",
@ -311,10 +313,13 @@
"interaction_modal.description.follow": "Con una cuenta en Mastodon, podés seguir a {name} para recibir sus mensajes en tu línea temporal principal.",
"interaction_modal.description.reblog": "Con una cuenta en Mastodon, podés adherir a este mensaje para compartirlo con tus propios seguidores.",
"interaction_modal.description.reply": "Con una cuenta en Mastodon, podés responder a este mensaje.",
"interaction_modal.login.action": "Llevame al comienzo",
"interaction_modal.login.prompt": "Dominio de su servidor de inicio, p. ej., mastodon.social",
"interaction_modal.no_account_yet": "¿No tenés cuenta en Mastodon?",
"interaction_modal.on_another_server": "En un servidor diferente",
"interaction_modal.on_this_server": "En este servidor",
"interaction_modal.other_server_instructions": "Copiá y pegá esta dirección web en la barra de búsqueda de tu aplicación favorita de Mastodon, o en la interface web de tu servidor de Mastodon.",
"interaction_modal.preamble": "Ya que Mastodon es descentralizado, podés usar tu cuenta existente alojada por otro servidor Mastodon (u otra plataforma compatible, si no tenés una cuenta en ésta).",
"interaction_modal.sign_in": "No iniciaste sesión en este servidor. ¿Dónde se encuentra alojada tu cuenta?",
"interaction_modal.sign_in_hint": "Ayuda: es el sitio web en donde te registraste. Si no te acordás, buscá el correo electrónico de bienvenida en tu cuenta de email. También podés usar tu nombre de usuario entero, p. ej., @tunombredeusuario@mastodon.social.",
"interaction_modal.title.favourite": "Marcar como favorito el mensaje de {name}",
"interaction_modal.title.follow": "Seguir a {name}",
"interaction_modal.title.reblog": "Adherir al mensaje de {name}",
@ -596,6 +601,7 @@
"server_banner.server_stats": "Estadísticas del servidor:",
"sign_in_banner.create_account": "Crear cuenta",
"sign_in_banner.sign_in": "Iniciar sesión",
"sign_in_banner.sso_redirect": "Iniciá sesión o registrate",
"sign_in_banner.text": "Iniciá sesión para seguir cuentas o etiquetas, marcar mensajes como favoritos, compartirlos y responderlos. También podés interactuar desde tu cuenta en un servidor diferente.",
"status.admin_account": "Abrir interface de moderación para @{name}",
"status.admin_domain": "Abrir interface de moderación para {domain}",
@ -671,7 +677,7 @@
"timeline_hint.resources.followers": "Tus seguidores",
"timeline_hint.resources.follows": "Las cuentas que seguís",
"timeline_hint.resources.statuses": "Mensajes más antiguos",
"trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} personas}} en el/los pasado/s {days, plural, one {día} other {{days} días}}",
"trends.counter_by_accounts": "{count, plural, one {{counter} persona} other {{counter} personas}} en {days, plural, one {el pasado día} other {los pasados {days} días}}",
"trends.trending_now": "Tendencia ahora",
"ui.beforeunload": "Tu borrador se perderá si abandonás Mastodon.",
"units.short.billion": "{count}MM",

@ -191,7 +191,6 @@
"conversation.open": "Ver conversación",
"conversation.with": "Con {names}",
"copypaste.copied": "Copiado",
"copypaste.copy": "Copiar",
"copypaste.copy_to_clipboard": "Copiar al portapapeles",
"directory.federated": "Desde el fediverso conocido",
"directory.local": "Sólo de {domain}",
@ -202,6 +201,7 @@
"dismissable_banner.community_timeline": "Estas son las publicaciones públicas más recientes de las personas cuyas cuentas están alojadas en {domain}.",
"dismissable_banner.dismiss": "Descartar",
"dismissable_banner.explore_links": "Estas noticias están siendo discutidas por personas en este y otros servidores de la red descentralizada en este momento.",
"dismissable_banner.explore_statuses": "Estas son las publicaciones que están ganando popularidad en la web social hoy. Las publicaciones recientes con más impulsos y favoritos obtienen más exposición.",
"dismissable_banner.explore_tags": "Se trata de hashtags que están ganando adeptos en las redes sociales hoy en día. Los hashtags que son utilizados por más personas diferentes se clasifican mejor.",
"dismissable_banner.public_timeline": "Estos son los toots públicos más recientes de personas en la web social a las que sigue la gente en {domain}.",
"embed.instructions": "Añade este toot a tu sitio web con el siguiente código.",
@ -230,6 +230,8 @@
"empty_column.direct": "Aún no tienes menciones privadas. Cuando envíes o recibas una, aparecerán aquí.",
"empty_column.domain_blocks": "Todavía no hay dominios ocultos.",
"empty_column.explore_statuses": "Nada es tendencia en este momento. ¡Revisa más tarde!",
"empty_column.favourited_statuses": "Todavía no tienes publicaciones favoritas. Cuando marques una publicación como favorita, se mostrarán aquí.",
"empty_column.favourites": "Todavía nadie marcó esta publicación como favorita. Cuando alguien lo haga, se mostrarán aquí.",
"empty_column.follow_requests": "No tienes ninguna petición de seguidor. Cuando recibas una, se mostrará aquí.",
"empty_column.followed_tags": "No estás siguiendo ningún hashtag todavía. Cuando lo hagas, aparecerá aquí.",
"empty_column.hashtag": "No hay nada en este hashtag aún.",
@ -293,6 +295,9 @@
"hashtag.column_settings.tag_mode.any": "Cualquiera de estos",
"hashtag.column_settings.tag_mode.none": "Ninguno de estos",
"hashtag.column_settings.tag_toggle": "Incluye etiquetas adicionales para esta columna",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}} hoy",
"hashtag.follow": "Seguir etiqueta",
"hashtag.unfollow": "Dejar de seguir etiqueta",
"home.actions.go_to_explore": "Ver tendencias",
@ -300,16 +305,22 @@
"home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar retoots",
"home.column_settings.show_replies": "Mostrar respuestas",
"home.explore_prompt.body": "Tu cronología de inicio tendrá una mezcla de publicaciones de etiquetas que hayas decidido seguir, personas que hayas seguido y publicaciones que estas impulsen. Ahora está muy vacía, por qué no:",
"home.explore_prompt.body": "Tu cronología de inicio tendrá una mezcla de publicaciones de las etiquetas que has escogido seguir, la gente que has decidido seguir y las publicaciones que impulsen. Si crees que está demasiado tranquila, quizás quieras:",
"home.explore_prompt.title": "Este es tu punto de partida en Mastodon.",
"home.hide_announcements": "Ocultar anuncios",
"home.show_announcements": "Mostrar anuncios",
"interaction_modal.description.favourite": "Con una cuenta en Mastodon, puedes marcar como favorita esta publicación para que el autor sepa que te gusta, y guardala para más adelante.",
"interaction_modal.description.follow": "Con una cuenta en Mastodon, puedes seguir {name} para recibir sus publicaciones en tu fuente de inicio.",
"interaction_modal.description.reblog": "Con una cuenta en Mastodon, puedes impulsar esta publicación para compartirla con tus propios seguidores.",
"interaction_modal.description.reply": "Con una cuenta en Mastodon, puedes responder a esta publicación.",
"interaction_modal.login.action": "Ir a Inicio",
"interaction_modal.login.prompt": "Dominio de tu servidor, por ejemplo mastodon.social",
"interaction_modal.no_account_yet": "¿Aún no tienes cuenta en Mastodon?",
"interaction_modal.on_another_server": "En un servidor diferente",
"interaction_modal.on_this_server": "En este servidor",
"interaction_modal.preamble": "Ya que Mastodon es descentralizado, puedes usar tu cuenta existente alojada en otro servidor Mastodon o plataforma compatible si no tienes una cuenta en este servidor.",
"interaction_modal.sign_in": "No estás registrado en este servidor. ¿Dónde tienes tu cuenta?",
"interaction_modal.sign_in_hint": "Pista: Ese es el sitio donde te registraste. Si no lo recuerdas, busca el correo electrónico de bienvenida en tu bandeja de entrada. También puedes introducir tu nombre de usuario completo (por ejemplo @Mastodon@mastodon.social)",
"interaction_modal.title.favourite": "Marcar como favorita la publicación de {name}",
"interaction_modal.title.follow": "Seguir a {name}",
"interaction_modal.title.reblog": "Impulsar la publicación de {name}",
"interaction_modal.title.reply": "Responder la publicación de {name}",
@ -325,6 +336,8 @@
"keyboard_shortcuts.direct": "para abrir la columna de menciones privadas",
"keyboard_shortcuts.down": "mover hacia abajo en la lista",
"keyboard_shortcuts.enter": "abrir estado",
"keyboard_shortcuts.favourite": "Marcar como favorita la publicación",
"keyboard_shortcuts.favourites": "Abrir lista de favoritos",
"keyboard_shortcuts.federated": "abrir el timeline federado",
"keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "abrir el timeline propio",
@ -355,6 +368,7 @@
"lightbox.previous": "Anterior",
"limited_account_hint.action": "Mostrar perfil de todos modos",
"limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.",
"link_preview.author": "Por {name}",
"lists.account.add": "Añadir a lista",
"lists.account.remove": "Quitar de lista",
"lists.delete": "Borrar lista",
@ -387,6 +401,7 @@
"navigation_bar.domain_blocks": "Dominios ocultos",
"navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.explore": "Explorar",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.filters": "Palabras silenciadas",
"navigation_bar.follow_requests": "Solicitudes para seguirte",
"navigation_bar.followed_tags": "Hashtags seguidos",
@ -403,6 +418,7 @@
"not_signed_in_indicator.not_signed_in": "Necesitas iniciar sesión para acceder a este recurso.",
"notification.admin.report": "{name} denunció a {target}",
"notification.admin.sign_up": "{name} se unio",
"notification.favourite": "{name} marcó como favorita tu publicación",
"notification.follow": "{name} te empezó a seguir",
"notification.follow_request": "{name} ha solicitado seguirte",
"notification.mention": "{name} te ha mencionado",
@ -416,6 +432,7 @@
"notifications.column_settings.admin.report": "Nuevas denuncias:",
"notifications.column_settings.admin.sign_up": "Registros nuevos:",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
@ -433,6 +450,7 @@
"notifications.column_settings.update": "Ediciones:",
"notifications.filter.all": "Todos",
"notifications.filter.boosts": "Retoots",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguidores",
"notifications.filter.mentions": "Menciones",
"notifications.filter.polls": "Resultados de la votación",
@ -583,6 +601,8 @@
"server_banner.server_stats": "Estadísticas del servidor:",
"sign_in_banner.create_account": "Crear cuenta",
"sign_in_banner.sign_in": "Iniciar sesión",
"sign_in_banner.sso_redirect": "Iniciar sesión o Registrarse",
"sign_in_banner.text": "Inicia sesión para seguir perfiles o etiquetas, así como marcar como favoritas, compartir y responder a publicaciones. También puedes interactuar desde tu cuenta en un servidor diferente.",
"status.admin_account": "Abrir interfaz de moderación para @{name}",
"status.admin_domain": "Abrir interfaz de moderación para {domain}",
"status.admin_status": "Abrir este estado en la interfaz de moderación",
@ -599,6 +619,7 @@
"status.edited": "Editado {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} time} other {{count} veces}}",
"status.embed": "Incrustado",
"status.favourite": "Favorito",
"status.filter": "Filtrar esta publicación",
"status.filtered": "Filtrado",
"status.hide": "Ocultar toot",

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save