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

lolsob-rspec
kouhai dev 2023-08-26 22:00:56 -07:00
commit 7c7355a964
438 changed files with 6788 additions and 5765 deletions

View File

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

View File

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

View File

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

View File

@ -1,20 +1,21 @@
{ {
$schema: 'https://docs.renovatebot.com/renovate-schema.json', $schema: 'https://docs.renovatebot.com/renovate-schema.json',
extends: [ extends: [
'config:base', 'config:recommended',
':dependencyDashboard',
':labels(dependencies)', ':labels(dependencies)',
':maintainLockFilesMonthly', // update non-direct dependencies monthly ':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, // 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 // 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 // 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. // 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: [ packageRules: [
{ {
// Ignore major version bumps for these node packages // Require Dependency Dashboard Approval for major version bumps of these node packages
matchManagers: ['npm'], matchManagers: ['npm'],
matchPackageNames: [ matchPackageNames: [
'tesseract.js', // Requires code changes 'tesseract.js', // Requires code changes
@ -41,10 +42,10 @@
'react-router-dom', 'react-router-dom',
], ],
matchUpdateTypes: ['major'], 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'], matchManagers: ['bundler'],
matchPackageNames: [ matchPackageNames: [
'rack', // Needs to be synced with Rails version 'rack', // Needs to be synced with Rails version
@ -55,7 +56,7 @@
'redis', // Requires manual upgrade and sync with Sidekiq version 'redis', // Requires manual upgrade and sync with Sidekiq version
], ],
matchUpdateTypes: ['major'], matchUpdateTypes: ['major'],
enabled: false, dependencyDashboardApproval: true,
}, },
{ {
// Update Github Actions and Docker images weekly // Update Github Actions and Docker images weekly
@ -63,25 +64,25 @@
extends: ['schedule:weekly'], 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'], matchManagers: ['dockerfile'],
matchPackageNames: ['moritzheiber/ruby-jemalloc'], matchPackageNames: ['moritzheiber/ruby-jemalloc'],
matchUpdateTypes: ['minor', 'major'], 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'], matchManagers: ['dockerfile'],
matchPackageNames: ['node'], matchPackageNames: ['node'],
matchUpdateTypes: ['major'], 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'], matchManagers: ['docker-compose'],
matchPackageNames: ['postgres'], matchPackageNames: ['postgres'],
matchUpdateTypes: ['major'], matchUpdateTypes: ['major'],
enabled: false, dependencyDashboardApproval: true,
}, },
{ {
// Update devDependencies every week, with one grouped PR // Update devDependencies every week, with one grouped PR

View File

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

View File

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` # `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 # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # 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/access_token_fabricator.rb'
- 'spec/fabricators/conversation_fabricator.rb' - 'spec/fabricators/conversation_fabricator.rb'
- 'spec/fabricators/system_key_fabricator.rb' - 'spec/fabricators/system_key_fabricator.rb'
- 'spec/helpers/admin/action_logs_helper_spec.rb'
- 'spec/lib/activitypub/adapter_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/user_role_spec.rb'
- 'spec/models/web/setting_spec.rb'
Lint/NonLocalExitFromIterator: Lint/NonLocalExitFromIterator:
Exclude: Exclude:
@ -135,7 +105,7 @@ Lint/UselessAssignment:
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize: Metrics/AbcSize:
Max: 143 Max: 144
Exclude: Exclude:
- 'app/serializers/initial_state_serializer.rb' - 'app/serializers/initial_state_serializer.rb'
@ -166,6 +136,19 @@ Naming/VariableNumber:
- 'spec/models/domain_block_spec.rb' - 'spec/models/domain_block_spec.rb'
- 'spec/models/user_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: RSpec/AnyInstance:
Exclude: Exclude:
- 'spec/controllers/activitypub/inboxes_controller_spec.rb' - 'spec/controllers/activitypub/inboxes_controller_spec.rb'
@ -765,6 +748,15 @@ Style/RedundantFetchBlock:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/puma.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). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
# AllowedMethods: present?, blank?, presence, try, try! # AllowedMethods: present?, blank?, presence, try, try!

View File

@ -2,6 +2,259 @@
All notable changes to this project will be documented in this file. 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 ## [4.1.6] - 2023-07-31
### Fixed ### Fixed

16
Gemfile
View File

@ -35,11 +35,14 @@ group :pam_authentication, optional: true do
end end
gem 'net-ldap', '~> 0.18' 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_openid_connect', '~> 0.6.1'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 2.0'
gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'color_diff', '~> 0.1' gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2' gem 'discard', '~> 1.2'
@ -56,8 +59,9 @@ gem 'httplog', '~> 1.6.2'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' 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 'nokogiri', '~> 1.15'
gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'
@ -106,7 +110,7 @@ group :test do
gem 'fuubar', '~> 2.5' gem 'fuubar', '~> 2.5'
# Extra RSpec extenion methods and helpers for sidekiq # Extra RSpec extenion methods and helpers for sidekiq
gem 'rspec-sidekiq', '~> 3.1' gem 'rspec-sidekiq', '~> 4.0'
# Browser integration testing # Browser integration testing
gem 'capybara', '~> 3.39' gem 'capybara', '~> 3.39'

View File

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

View File

@ -1,8 +1,11 @@
# Security Policy # 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 ## Scope

43
Vagrantfile vendored
View File

@ -60,6 +60,37 @@ sudo usermod -a -G rvm $USER
SCRIPT 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 $provisionB = <<SCRIPT
source "/etc/profile.d/rvm.sh" source "/etc/profile.d/rvm.sh"
@ -102,10 +133,8 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "4096"] vb.customize ["modifyvm", :id, "--memory", "8192"]
# Increase the number of CPUs. Uncomment and adjust to vb.customize ["modifyvm", :id, "--cpus", "3"]
# increase performance
# vb.customize ["modifyvm", :id, "--cpus", "3"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions. # Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172 # 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: 3000, host: 3000
config.vm.network :forwarded_port, guest: 4000, host: 4000 config.vm.network :forwarded_port, guest: 4000, host: 4000
config.vm.network :forwarded_port, guest: 8080, host: 8080 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' # Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision'
config.vm.provision :shell, inline: $provisionA, privileged: false, reset: true 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.provision :shell, inline: $provisionB, privileged: false
config.vm.post_up_message = <<MESSAGE config.vm.post_up_message = <<MESSAGE

View File

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

View File

@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class InstancesIndex < Chewy::Index class InstancesIndex < Chewy::Index
settings index: { refresh_interval: '30s' } settings index: index_preset(refresh_interval: '30s')
index_scope ::Instance.searchable index_scope ::Instance.searchable
root date_detection: false do 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' field :accounts_count, type: 'long'
end end
end end

View File

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

View File

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

View File

@ -40,7 +40,7 @@ module Admin
end end
# Allow transparently upgrading a domain block # 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 = existing_domain_block
@domain_block.assign_attributes(resource_params) @domain_block.assign_attributes(resource_params)
end end

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController
private private
def account_params 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 end
def set_account def set_account

View File

@ -21,6 +21,8 @@ module ContextHelper
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } }, focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' }, blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' }, 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' }, voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: { 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', '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',

View File

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

View File

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

View File

@ -77,7 +77,10 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.hidden = normalOldStatus.get('hidden'); normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.quote = normalOldStatus.get('quote'); normalStatus.quote = normalOldStatus.get('quote');
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden'); normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
normalStatus.translation = normalOldStatus.get('translation');
if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation');
}
} else { } else {
const spoilerText = normalStatus.spoiler_text || ''; 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'); 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');

View File

@ -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 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 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 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 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 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); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

View File

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

View File

@ -18,7 +18,19 @@ export default class Column extends PureComponent {
}; };
scrollTop () { 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) { if (!scrollable) {
return; return;

View File

@ -196,9 +196,9 @@ class Header extends ImmutablePureComponent {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = ''; actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) { } 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'])) { } 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'])) { } else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
} }

View File

@ -23,6 +23,7 @@ export default class Story extends PureComponent {
author: PropTypes.string, author: PropTypes.string,
sharedTimes: PropTypes.number, sharedTimes: PropTypes.number,
thumbnail: PropTypes.string, thumbnail: PropTypes.string,
thumbnailDescription: PropTypes.string,
blurhash: PropTypes.string, blurhash: PropTypes.string,
expanded: PropTypes.bool, expanded: PropTypes.bool,
}; };
@ -34,7 +35,7 @@ export default class Story extends PureComponent {
handleImageLoad = () => this.setState({ thumbnailLoaded: true }); handleImageLoad = () => this.setState({ thumbnailLoaded: true });
render () { 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; const { thumbnailLoaded } = this.state;
@ -50,7 +51,7 @@ export default class Story extends PureComponent {
{thumbnail ? ( {thumbnail ? (
<> <>
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div> <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 />} ) : <Skeleton />}
</div> </div>

View File

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

View File

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

View File

@ -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,
};

View File

@ -1,9 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; 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 { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes'; 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 { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column'; import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header'; 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 StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import { HashtagHeader } from './components/hashtag_header';
import ColumnSettingsContainer from './containers/column_settings_container'; 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) => ({ const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
tag: state.getIn(['tags', props.params.id]), tag: state.getIn(['tags', props.params.id]),
@ -48,7 +41,6 @@ class HashtagTimeline extends PureComponent {
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map, tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
intl: PropTypes.object,
}; };
handlePin = () => { handlePin = () => {
@ -188,27 +180,11 @@ class HashtagTimeline extends PureComponent {
}; };
render () { render () {
const { hasUnread, columnId, multiColumn, tag, intl } = this.props; const { hasUnread, columnId, multiColumn, tag } = this.props;
const { id, local } = this.props.params; const { id, local } = this.props.params;
const pinned = !!columnId; const pinned = !!columnId;
const { signedIn } = this.context.identity; 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 ( return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}> <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
<ColumnHeader <ColumnHeader
@ -220,13 +196,14 @@ class HashtagTimeline extends PureComponent {
onClick={this.handleHeaderClick} onClick={this.handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={followButton}
showBackButton showBackButton
> >
{columnId && <ColumnSettingsContainer columnId={columnId} />} {columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
alwaysPrepend
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`} scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}${local ? ':local' : ''}`} timelineId={`hashtag:${id}${local ? ':local' : ''}`}
@ -245,4 +222,4 @@ class HashtagTimeline extends PureComponent {
} }
export default connect(mapStateToProps)(injectIntl(HashtagTimeline)); export default connect(mapStateToProps)(HashtagTimeline);

View File

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

View File

@ -178,7 +178,8 @@ export default class Card extends PureComponent {
dummy={!useBlurhash} 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 = ( let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'> <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'> <span className='spoiler-button__overlay__label'>

View File

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

View File

@ -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));

View File

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

View File

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

View File

@ -105,14 +105,7 @@ export default class ModalRoot extends PureComponent {
handleClose = (ignoreFocus = false) => { handleClose = (ignoreFocus = false) => {
const { onClose } = this.props; const { onClose } = this.props;
let message = null; const message = this._modal?.getCloseConfirmationMessage?.();
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`.
}
onClose(message, ignoreFocus); onClose(message, ignoreFocus);
}; };
@ -133,7 +126,10 @@ export default class ModalRoot extends PureComponent {
{visible && ( {visible && (
<> <>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> <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> </BundleContainer>
<Helmet> <Helmet>

View File

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

View File

@ -3,7 +3,7 @@ import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { openModal } from 'flavours/glitch/actions/modal'; 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'; import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
const SignInBanner = () => { const SignInBanner = () => {
@ -18,6 +18,15 @@ const SignInBanner = () => {
const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up'); 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) { if (registrationsOpen) {
signupButton = ( signupButton = (
<a href={signupUrl} className='button button--block'> <a href={signupUrl} className='button button--block'>

View File

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

View File

@ -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.", "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.done": "Udført",
"follow_recommendations.heading": "Følg personer du gerne vil se indlæg fra! Her er nogle forslag.", "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!", "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_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}.", "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.always_show_spoilers_field": "Vis altid feltet til indholdsadvarsel",
"settings.preferences": "Preferences" "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"
} }

View File

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

View File

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

View File

@ -13,4 +13,30 @@ ready(() => {
console.error(error); console.error(error);
}); });
}, 5000); }, 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);
});
}); });

View File

@ -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;
}
}
}

View File

@ -147,10 +147,6 @@
display: block; display: block;
width: 100%; width: 100%;
} }
.layout-multiple-columns &.button--with-bell {
font-size: 12px;
}
} }
.icon-button { .icon-button {
@ -1345,7 +1341,7 @@ button.icon-button.active i.fa-retweet {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba($black, 0.5); background: transparent;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0; padding: 0;
@ -1354,6 +1350,10 @@ button.icon-button.active i.fa-retweet {
color: $white; color: $white;
&__label { &__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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -1367,6 +1367,13 @@ button.icon-button.active i.fa-retweet {
font-weight: 400; font-weight: 400;
font-size: 13px; font-size: 13px;
} }
&:hover,
&:focus {
.spoiler-button__overlay__label {
background-color: rgba($black, 0.9);
}
}
} }
} }

View File

@ -719,15 +719,16 @@
} }
.button.button-secondary { .button.button-secondary {
border-color: $ui-button-secondary-border-color; border-color: $inverted-text-color;
color: $ui-button-secondary-color; color: $inverted-text-color;
flex: 0 0 auto; flex: 0 0 auto;
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
border-color: $ui-button-secondary-focus-background-color; background: transparent;
color: $ui-button-secondary-focus-color; 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 { .link-button {
font-size: inherit; font-size: inherit;
display: inline; display: inline;

View File

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

View File

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

View File

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

View File

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

View File

@ -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 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 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 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 expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => 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); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

View File

@ -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>"`,
);
});
});

View File

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

View File

@ -16,7 +16,19 @@ export default class Column extends PureComponent {
}; };
scrollTop () { 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) { if (!scrollable) {
return; return;

View File

@ -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>
);
};

View File

@ -22,6 +22,7 @@ import { displayMedia } from '../initial_state';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay'; import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name'; import { DisplayName } from './display_name';
import { getHashtagBarForStatus } from './hashtag_bar';
import { RelativeTimestamp } from './relative_timestamp'; import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import StatusContent from './status_content'; import StatusContent from './status_content';
@ -544,6 +545,9 @@ class Status extends ImmutablePureComponent {
const visibilityIcon = visibilityIconInfo[status.get('visibility')]; const visibilityIcon = visibilityIconInfo[status.get('visibility')];
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
const expanded = !status.get('hidden')
return ( return (
<HotKeys handlers={handlers}> <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}> <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 <StatusContent
status={status} status={status}
onClick={this.handleClick} onClick={this.handleClick}
expanded={!status.get('hidden')} expanded={expanded}
onExpandedToggle={this.handleExpandedToggle} onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate} onTranslate={this.handleTranslate}
collapsible collapsible
onCollapsedToggle={this.handleCollapsedToggle} onCollapsedToggle={this.handleCollapsedToggle}
{...statusContentProps}
/> />
{media} {media}
{expanded && hashtagBar}
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} /> <StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
</div> </div>
</div> </div>

View File

@ -15,6 +15,15 @@ import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_s
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) 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 { class TranslateButton extends PureComponent {
static propTypes = { static propTypes = {
@ -65,6 +74,7 @@ class StatusContent extends PureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
statusContent: PropTypes.string,
expanded: PropTypes.bool, expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func, onExpandedToggle: PropTypes.func,
onTranslate: PropTypes.func, onTranslate: PropTypes.func,
@ -225,7 +235,7 @@ class StatusContent extends PureComponent {
}; };
render () { render () {
const { status, intl } = this.props; const { status, intl, statusContent } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed'); 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 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 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 spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', { const classNames = classnames('status__content', {

View File

@ -265,9 +265,9 @@ class Header extends ImmutablePureComponent {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = ''; actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) { } 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'])) { } 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'])) { } else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
} }

View File

@ -22,6 +22,7 @@ export default class Story extends PureComponent {
author: PropTypes.string, author: PropTypes.string,
sharedTimes: PropTypes.number, sharedTimes: PropTypes.number,
thumbnail: PropTypes.string, thumbnail: PropTypes.string,
thumbnailDescription: PropTypes.string,
blurhash: PropTypes.string, blurhash: PropTypes.string,
expanded: PropTypes.bool, expanded: PropTypes.bool,
}; };
@ -33,7 +34,7 @@ export default class Story extends PureComponent {
handleImageLoad = () => this.setState({ thumbnailLoaded: true }); handleImageLoad = () => this.setState({ thumbnailLoaded: true });
render () { 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; const { thumbnailLoaded } = this.state;
@ -49,7 +50,7 @@ export default class Story extends PureComponent {
{thumbnail ? ( {thumbnail ? (
<> <>
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div> <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 />} ) : <Skeleton />}
</div> </div>

View File

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

View File

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

View File

@ -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,
};

View File

@ -1,9 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; 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 { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes'; 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 { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import StatusListContainer from '../ui/containers/status_list_container'; import StatusListContainer from '../ui/containers/status_list_container';
import { HashtagHeader } from './components/hashtag_header';
import ColumnSettingsContainer from './containers/column_settings_container'; 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) => ({ const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
tag: state.getIn(['tags', props.params.id]), tag: state.getIn(['tags', props.params.id]),
@ -48,7 +42,6 @@ class HashtagTimeline extends PureComponent {
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map, tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
intl: PropTypes.object,
}; };
handlePin = () => { handlePin = () => {
@ -188,27 +181,11 @@ class HashtagTimeline extends PureComponent {
}; };
render () { render () {
const { hasUnread, columnId, multiColumn, tag, intl } = this.props; const { hasUnread, columnId, multiColumn, tag } = this.props;
const { id, local } = this.props.params; const { id, local } = this.props.params;
const pinned = !!columnId; const pinned = !!columnId;
const { signedIn } = this.context.identity; 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 ( return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}> <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
<ColumnHeader <ColumnHeader
@ -220,13 +197,14 @@ class HashtagTimeline extends PureComponent {
onClick={this.handleHeaderClick} onClick={this.handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={followButton}
showBackButton showBackButton
> >
{columnId && <ColumnSettingsContainer columnId={columnId} />} {columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
alwaysPrepend
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`} scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}${local ? ':local' : ''}`} timelineId={`hashtag:${id}${local ? ':local' : ''}`}
@ -245,4 +223,4 @@ class HashtagTimeline extends PureComponent {
} }
export default connect(mapStateToProps)(injectIntl(HashtagTimeline)); export default connect(mapStateToProps)(HashtagTimeline);

View File

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

View File

@ -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 = ( let spoilerButton = (
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'> <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>

View File

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

View File

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

View File

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

View File

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

View File

@ -97,14 +97,7 @@ export default class ModalRoot extends PureComponent {
handleClose = (ignoreFocus = false) => { handleClose = (ignoreFocus = false) => {
const { onClose } = this.props; const { onClose } = this.props;
let message = null; const message = this._modal?.getCloseConfirmationMessage?.();
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`.
}
onClose(message, ignoreFocus); onClose(message, ignoreFocus);
}; };
@ -122,7 +115,10 @@ export default class ModalRoot extends PureComponent {
{visible && ( {visible && (
<> <>
<BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> <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> </BundleContainer>
<Helmet> <Helmet>

View File

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

View File

@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
import { openModal } from 'mastodon/actions/modal'; 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'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
const SignInBanner = () => { const SignInBanner = () => {
@ -19,6 +19,15 @@ const SignInBanner = () => {
const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up'); 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) { if (registrationsOpen) {
signupButton = ( signupButton = (
<a href={signupUrl} className='button button--block'> <a href={signupUrl} className='button button--block'>

View File

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

View File

@ -181,7 +181,6 @@
"home.column_settings.show_reblogs": "Wys aangestuurde plasings", "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.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.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.reblog": "Stuur {name} se plasing aan",
"interaction_modal.title.reply": "Reageer op {name} se plasing", "interaction_modal.title.reply": "Reageer op {name} se plasing",
"keyboard_shortcuts.back": "Navigeer terug", "keyboard_shortcuts.back": "Navigeer terug",

View File

@ -172,7 +172,6 @@
"conversation.open": "Veyer conversación", "conversation.open": "Veyer conversación",
"conversation.with": "Con {names}", "conversation.with": "Con {names}",
"copypaste.copied": "Copiau", "copypaste.copied": "Copiau",
"copypaste.copy": "Copiar",
"directory.federated": "Dende lo fediverso conoixiu", "directory.federated": "Dende lo fediverso conoixiu",
"directory.local": "Nomás de {domain}", "directory.local": "Nomás de {domain}",
"directory.new_arrivals": "Recientment plegaus", "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.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_another_server": "En un servidor diferent",
"interaction_modal.on_this_server": "En este servidor", "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.follow": "Seguir a {name}",
"interaction_modal.title.reblog": "Empentar la publicación de {name}", "interaction_modal.title.reblog": "Empentar la publicación de {name}",
"interaction_modal.title.reply": "Responder a la publicación de {name}", "interaction_modal.title.reply": "Responder a la publicación de {name}",

View File

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

View File

@ -133,7 +133,6 @@
"conversation.open": "Ver la conversación", "conversation.open": "Ver la conversación",
"conversation.with": "Con {names}", "conversation.with": "Con {names}",
"copypaste.copied": "Copióse", "copypaste.copied": "Copióse",
"copypaste.copy": "Copiar",
"directory.federated": "Del fediversu conocíu", "directory.federated": "Del fediversu conocíu",
"directory.local": "De «{domain}» namás", "directory.local": "De «{domain}» namás",
"directory.new_arrivals": "Cuentes nueves", "directory.new_arrivals": "Cuentes nueves",
@ -213,6 +212,7 @@
"hashtag.column_header.tag_mode.none": "ensin {additional}", "hashtag.column_header.tag_mode.none": "ensin {additional}",
"hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia", "hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column", "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.follow": "Siguir a la etiqueta",
"hashtag.unfollow": "Dexar de siguir a la etiqueta", "hashtag.unfollow": "Dexar de siguir a la etiqueta",
"home.column_settings.basic": "Configuración básica", "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.description.reply": "Con una cuenta de Mastodon, pues responder a esti artículu.",
"interaction_modal.on_another_server": "N'otru sirvidor", "interaction_modal.on_another_server": "N'otru sirvidor",
"interaction_modal.on_this_server": "Nesti 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}", "interaction_modal.title.reply": "Rempuesta al artículu de: {name}",
"intervals.full.days": "{number, plural, one {# día} other {# díes}}", "intervals.full.days": "{number, plural, one {# día} other {# díes}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}", "intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
@ -420,6 +419,7 @@
"server_banner.learn_more": "Saber más", "server_banner.learn_more": "Saber más",
"server_banner.server_stats": "Estadístiques del sirvidor:", "server_banner.server_stats": "Estadístiques del sirvidor:",
"sign_in_banner.create_account": "Crear una cuenta", "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_account": "Abrir la interfaz de moderación pa @{name}",
"status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»", "status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»",
"status.admin_status": "Abrir esti artículu na interfaz de moderación", "status.admin_status": "Abrir esti artículu na interfaz de moderación",

View File

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

View File

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

View File

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

View File

@ -80,7 +80,7 @@
"admin.impact_report.instance_followers": "Seguidors que els nostres usuaris perdrien", "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.instance_follows": "Seguidors que els seus usuaris perdrien",
"admin.impact_report.title": "Resum del impacte", "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.rate_limited.title": "Límit de freqüència",
"alert.unexpected.message": "S'ha produït un error inesperat.", "alert.unexpected.message": "S'ha produït un error inesperat.",
"alert.unexpected.title": "Vaja!", "alert.unexpected.title": "Vaja!",
@ -114,7 +114,7 @@
"column.directory": "Navega pels perfils", "column.directory": "Navega pels perfils",
"column.domain_blocks": "Dominis blocats", "column.domain_blocks": "Dominis blocats",
"column.favourites": "Favorits", "column.favourites": "Favorits",
"column.firehose": "Fluxos en directe", "column.firehose": "Tuts en directe",
"column.follow_requests": "Peticions de seguir-te", "column.follow_requests": "Peticions de seguir-te",
"column.home": "Inici", "column.home": "Inici",
"column.lists": "Llistes", "column.lists": "Llistes",
@ -138,11 +138,11 @@
"compose.published.body": "Tut publicat.", "compose.published.body": "Tut publicat.",
"compose.published.open": "Obre", "compose.published.open": "Obre",
"compose_form.direct_message_warning_learn_more": "Més informació", "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.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": "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.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.add_option": "Afegeix una opció",
"compose_form.poll.duration": "Durada de l'enquesta", "compose_form.poll.duration": "Durada de l'enquesta",
"compose_form.poll.option_placeholder": "Opció {number}", "compose_form.poll.option_placeholder": "Opció {number}",
@ -191,7 +191,6 @@
"conversation.open": "Mostra la conversa", "conversation.open": "Mostra la conversa",
"conversation.with": "Amb {names}", "conversation.with": "Amb {names}",
"copypaste.copied": "Copiat", "copypaste.copied": "Copiat",
"copypaste.copy": "Copia",
"copypaste.copy_to_clipboard": "Copia al porta-retalls", "copypaste.copy_to_clipboard": "Copia al porta-retalls",
"directory.federated": "Del fedivers conegut", "directory.federated": "Del fedivers conegut",
"directory.local": "Només de {domain}", "directory.local": "Només de {domain}",
@ -296,6 +295,9 @@
"hashtag.column_settings.tag_mode.any": "Qualsevol daquests", "hashtag.column_settings.tag_mode.any": "Qualsevol daquests",
"hashtag.column_settings.tag_mode.none": "Cap daquests", "hashtag.column_settings.tag_mode.none": "Cap daquests",
"hashtag.column_settings.tag_toggle": "Inclou etiquetes addicionals per a aquesta columna", "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.follow": "Segueix l'etiqueta",
"hashtag.unfollow": "Deixa de seguir l'etiqueta", "hashtag.unfollow": "Deixa de seguir l'etiqueta",
"home.actions.go_to_explore": "Mira què és tendència", "home.actions.go_to_explore": "Mira què és tendència",
@ -303,7 +305,7 @@
"home.column_settings.basic": "Bàsic", "home.column_settings.basic": "Bàsic",
"home.column_settings.show_reblogs": "Mostra els impulsos", "home.column_settings.show_reblogs": "Mostra els impulsos",
"home.column_settings.show_replies": "Mostra les respostes", "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.explore_prompt.title": "Aquest és la teva base a Mastodon.",
"home.hide_announcements": "Amaga els anuncis", "home.hide_announcements": "Amaga els anuncis",
"home.show_announcements": "Mostra 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.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.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.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_another_server": "En un servidor diferent",
"interaction_modal.on_this_server": "En aquest servidor", "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.sign_in": "No has iniciat sessió en aquest servidor. On tens el teu compte?",
"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_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.favourite": "Afavoreix el tut de {name}",
"interaction_modal.title.follow": "Segueix {name}", "interaction_modal.title.follow": "Segueix {name}",
"interaction_modal.title.reblog": "Impulsa el tut de {name}", "interaction_modal.title.reblog": "Impulsa el tut de {name}",
@ -596,6 +601,7 @@
"server_banner.server_stats": "Estadístiques del servidor:", "server_banner.server_stats": "Estadístiques del servidor:",
"sign_in_banner.create_account": "Crea un compte", "sign_in_banner.create_account": "Crea un compte",
"sign_in_banner.sign_in": "Inici de sessió", "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.", "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_account": "Obre la interfície de moderació per a @{name}",
"status.admin_domain": "Obre la interfície de moderació per a @{domain}", "status.admin_domain": "Obre la interfície de moderació per a @{domain}",

View File

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

View File

@ -191,7 +191,6 @@
"conversation.open": "Zobrazit konverzaci", "conversation.open": "Zobrazit konverzaci",
"conversation.with": "S {names}", "conversation.with": "S {names}",
"copypaste.copied": "Zkopírováno", "copypaste.copied": "Zkopírováno",
"copypaste.copy": "Zkopírovat",
"copypaste.copy_to_clipboard": "Zkopírovat do schránky", "copypaste.copy_to_clipboard": "Zkopírovat do schránky",
"directory.federated": "Ze známého fedivesmíru", "directory.federated": "Ze známého fedivesmíru",
"directory.local": "Pouze z {domain}", "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.community_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí, jejichž účty hostuje {domain}.",
"dismissable_banner.dismiss": "Zavřít", "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_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.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.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:", "embed.preview": "Takhle to bude vypadat:",
"emoji_button.activity": "Aktivita", "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.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.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.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.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.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í.", "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.column_settings.tag_toggle": "Zahrnout v tomto sloupci další štítky",
"hashtag.follow": "Sledovat hashtag", "hashtag.follow": "Sledovat hashtag",
"hashtag.unfollow": "Přestat 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.basic": "Základní",
"home.column_settings.show_reblogs": "Zobrazit boosty", "home.column_settings.show_reblogs": "Zobrazit boosty",
"home.column_settings.show_replies": "Zobrazit odpovědi", "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.hide_announcements": "Skrýt oznámení",
"home.show_announcements": "Zobrazit 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.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.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.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_another_server": "Na jiném serveru",
"interaction_modal.on_this_server": "Na tomto 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.follow": "Sledovat {name}",
"interaction_modal.title.reblog": "Boostnout příspěvek uživatele {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}", "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.direct": "otevřít sloupec soukromých zmínek",
"keyboard_shortcuts.down": "Posunout v seznamu dolů", "keyboard_shortcuts.down": "Posunout v seznamu dolů",
"keyboard_shortcuts.enter": "Otevřít příspěvek", "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.federated": "Otevřít federovanou časovou osu",
"keyboard_shortcuts.heading": "Klávesové zkratky", "keyboard_shortcuts.heading": "Klávesové zkratky",
"keyboard_shortcuts.home": "Otevřít domovskou časovou osu", "keyboard_shortcuts.home": "Otevřít domovskou časovou osu",
@ -350,11 +365,13 @@
"lightbox.previous": "Předchozí", "lightbox.previous": "Předchozí",
"limited_account_hint.action": "Přesto profil zobrazit", "limited_account_hint.action": "Přesto profil zobrazit",
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.", "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.add": "Přidat do seznamu",
"lists.account.remove": "Odebrat ze seznamu", "lists.account.remove": "Odebrat ze seznamu",
"lists.delete": "Smazat seznam", "lists.delete": "Smazat seznam",
"lists.edit": "Upravit seznam", "lists.edit": "Upravit seznam",
"lists.edit.submit": "Změnit název", "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.create": "Přidat seznam",
"lists.new.title_placeholder": "Název nového seznamu", "lists.new.title_placeholder": "Název nového seznamu",
"lists.replies_policy.followed": "Sledovaným uživatelům", "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.hide_notifications": "Skrýt oznámení od tohoto uživatele?",
"mute_modal.indefinite": "Neomezeně", "mute_modal.indefinite": "Neomezeně",
"navigation_bar.about": "O aplikaci", "navigation_bar.about": "O aplikaci",
"navigation_bar.advanced_interface": "Otevřít pokročilé webové rozhraní",
"navigation_bar.blocks": "Blokovaní uživatelé", "navigation_bar.blocks": "Blokovaní uživatelé",
"navigation_bar.bookmarks": "Záložky", "navigation_bar.bookmarks": "Záložky",
"navigation_bar.community_timeline": "Místní časová osa", "navigation_bar.community_timeline": "Místní časová osa",
@ -380,6 +398,7 @@
"navigation_bar.domain_blocks": "Blokované domény", "navigation_bar.domain_blocks": "Blokované domény",
"navigation_bar.edit_profile": "Upravit profil", "navigation_bar.edit_profile": "Upravit profil",
"navigation_bar.explore": "Prozkoumat", "navigation_bar.explore": "Prozkoumat",
"navigation_bar.favourites": "Oblíbené",
"navigation_bar.filters": "Skrytá slova", "navigation_bar.filters": "Skrytá slova",
"navigation_bar.follow_requests": "Žádosti o sledování", "navigation_bar.follow_requests": "Žádosti o sledování",
"navigation_bar.followed_tags": "Sledované hashtagy", "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.", "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.report": "Uživatel {name} nahlásil {target}",
"notification.admin.sign_up": "Uživatel {name} se zaregistroval", "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": "Uživatel {name} vás začal sledovat",
"notification.follow_request": "Uživatel {name} požádal o povolení vás sledovat", "notification.follow_request": "Uživatel {name} požádal o povolení vás sledovat",
"notification.mention": "Uživatel {name} vás zmínil", "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.report": "Nová hlášení:",
"notifications.column_settings.admin.sign_up": "Nové registrace:", "notifications.column_settings.admin.sign_up": "Nové registrace:",
"notifications.column_settings.alert": "Oznámení na počítači", "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.advanced": "Zobrazit všechny kategorie",
"notifications.column_settings.filter_bar.category": "Panel rychlého filtrování", "notifications.column_settings.filter_bar.category": "Panel rychlého filtrování",
"notifications.column_settings.filter_bar.show_bar": "Zobrazit panel filtrů", "notifications.column_settings.filter_bar.show_bar": "Zobrazit panel filtrů",
@ -426,6 +447,7 @@
"notifications.column_settings.update": "Úpravy:", "notifications.column_settings.update": "Úpravy:",
"notifications.filter.all": "Vše", "notifications.filter.all": "Vše",
"notifications.filter.boosts": "Boosty", "notifications.filter.boosts": "Boosty",
"notifications.filter.favourites": "Oblíbené",
"notifications.filter.follows": "Sledování", "notifications.filter.follows": "Sledování",
"notifications.filter.mentions": "Zmínky", "notifications.filter.mentions": "Zmínky",
"notifications.filter.polls": "Výsledky anket", "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.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.follows.title": "Populární na Mastodonu",
"onboarding.share.lead": "Dejte lidem vědět, jak vás mohou najít 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.next_steps": "Možné další kroky:",
"onboarding.share.title": "Sdílejte svůj profil", "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.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.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.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.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Řekněte světu Ahoj.", "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.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.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Sdílejte svůj profil", "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.exceeds_maxlength": "Potvrzení hesla překračuje maximální délku hesla",
"password_confirmation.mismatching": "Zadaná hesla se neshodují", "password_confirmation.mismatching": "Zadaná hesla se neshodují",
"picture_in_picture.restore": "Vrátit zpět", "picture_in_picture.restore": "Vrátit zpět",
"poll.closed": "Uzavřeno", "poll.closed": "Uzavřeno",
"poll.refresh": "Obnovit", "poll.refresh": "Obnovit",
"poll.reveal": "Zobrazit výsledky",
"poll.total_people": "{count, plural, one {# člověk} few {# lidé} many {# lidí} other {# lidí}}", "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.total_votes": "{count, plural, one {# hlas} few {# hlasy} many {# hlasů} other {# hlasů}}",
"poll.vote": "Hlasovat", "poll.vote": "Hlasovat",
@ -517,6 +546,8 @@
"report.placeholder": "Další komentáře", "report.placeholder": "Další komentáře",
"report.reasons.dislike": "Nelíbí se mi to", "report.reasons.dislike": "Nelíbí se mi to",
"report.reasons.dislike_description": "Není to něco, co chcete vidět", "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": "Je to něco jiného",
"report.reasons.other_description": "Problém neodpovídá ostatním kategoriím", "report.reasons.other_description": "Problém neodpovídá ostatním kategoriím",
"report.reasons.spam": "Je to spam", "report.reasons.spam": "Je to spam",
@ -536,6 +567,7 @@
"report.unfollow": "Přestat sledovat @{name}", "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.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.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.other": "Ostatní",
"report_notification.categories.spam": "Spam", "report_notification.categories.spam": "Spam",
"report_notification.categories.violation": "Porušení pravidla", "report_notification.categories.violation": "Porušení pravidla",
@ -566,6 +598,8 @@
"server_banner.server_stats": "Statistiky serveru:", "server_banner.server_stats": "Statistiky serveru:",
"sign_in_banner.create_account": "Vytvořit účet", "sign_in_banner.create_account": "Vytvořit účet",
"sign_in_banner.sign_in": "Přihlásit se", "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_account": "Otevřít moderátorské rozhraní pro @{name}",
"status.admin_domain": "Otevřít moderátorské rozhraní pro {domain}", "status.admin_domain": "Otevřít moderátorské rozhraní pro {domain}",
"status.admin_status": "Otevřít tento příspěvek v moderátorském rozhraní", "status.admin_status": "Otevřít tento příspěvek v moderátorském rozhraní",
@ -582,6 +616,7 @@
"status.edited": "Upraveno {date}", "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.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.embed": "Vložit na web",
"status.favourite": "Oblíbit",
"status.filter": "Filtrovat tento příspěvek", "status.filter": "Filtrovat tento příspěvek",
"status.filtered": "Filtrováno", "status.filtered": "Filtrováno",
"status.hide": "Skrýt příspěvek", "status.hide": "Skrýt příspěvek",

View File

@ -191,7 +191,6 @@
"conversation.open": "Gweld sgwrs", "conversation.open": "Gweld sgwrs",
"conversation.with": "Gyda {names}", "conversation.with": "Gyda {names}",
"copypaste.copied": "Wedi ei gopïo", "copypaste.copied": "Wedi ei gopïo",
"copypaste.copy": "Copïo",
"copypaste.copy_to_clipboard": "Copïo i'r clipfwrdd", "copypaste.copy_to_clipboard": "Copïo i'r clipfwrdd",
"directory.federated": "O'r ffedysawd cyfan", "directory.federated": "O'r ffedysawd cyfan",
"directory.local": "O {domain} yn unig", "directory.local": "O {domain} yn unig",
@ -299,22 +298,25 @@
"hashtag.follow": "Dilyn hashnod", "hashtag.follow": "Dilyn hashnod",
"hashtag.unfollow": "Dad-ddilyn hashnod", "hashtag.unfollow": "Dad-ddilyn hashnod",
"home.actions.go_to_explore": "Gweld beth sy'n tueddu", "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.basic": "Syml",
"home.column_settings.show_reblogs": "Dangos hybiau", "home.column_settings.show_reblogs": "Dangos hybiau",
"home.column_settings.show_replies": "Dangos atebion", "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.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 feewnn Mastodon.", "home.explore_prompt.title": "Dyma'ch cartref o fewn Mastodon.",
"home.hide_announcements": "Cuddio cyhoeddiadau", "home.hide_announcements": "Cuddio cyhoeddiadau",
"home.show_announcements": "Dangos 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.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.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.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.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_another_server": "Ar weinydd gwahanol",
"interaction_modal.on_this_server": "Ar y gweinydd hwn", "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.sign_in": "Nid ydych wedi mewngofnodi i'r gweinydd hwn. Ble mae eich cyfrif yn cael ei gynnal?",
"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_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.favourite": "Hoffi postiad {name}",
"interaction_modal.title.follow": "Dilyn {name}", "interaction_modal.title.follow": "Dilyn {name}",
"interaction_modal.title.reblog": "Hybu postiad {name}", "interaction_modal.title.reblog": "Hybu postiad {name}",
@ -363,6 +365,7 @@
"lightbox.previous": "Blaenorol", "lightbox.previous": "Blaenorol",
"limited_account_hint.action": "Dangos y proffil beth bynnag", "limited_account_hint.action": "Dangos y proffil beth bynnag",
"limited_account_hint.title": "Mae'r proffil hwn wedi cael ei guddio gan gymedrolwyr {domain}.", "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.add": "Ychwanegu at restr",
"lists.account.remove": "Tynnu o'r rhestr", "lists.account.remove": "Tynnu o'r rhestr",
"lists.delete": "Dileu rhestr", "lists.delete": "Dileu rhestr",
@ -387,7 +390,7 @@
"navigation_bar.about": "Ynghylch", "navigation_bar.about": "Ynghylch",
"navigation_bar.advanced_interface": "Agor mewn rhyngwyneb gwe uwch", "navigation_bar.advanced_interface": "Agor mewn rhyngwyneb gwe uwch",
"navigation_bar.blocks": "Defnyddwyr wedi eu blocio", "navigation_bar.blocks": "Defnyddwyr wedi eu blocio",
"navigation_bar.bookmarks": "Llyfrnodau", "navigation_bar.bookmarks": "Nodau Tudalen",
"navigation_bar.community_timeline": "Ffrwd leol", "navigation_bar.community_timeline": "Ffrwd leol",
"navigation_bar.compose": "Cyfansoddi post newydd", "navigation_bar.compose": "Cyfansoddi post newydd",
"navigation_bar.direct": "Crybwylliadau preifat", "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_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_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.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.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 colli dim", "notifications_permission_banner.title": "Peidiwch â cholli dim",
"onboarding.action.back": "Ewch â fi yn ôl", "onboarding.action.back": "Ewch â fi yn ôl",
"onboarding.actions.back": "Ewch â fi yn ôl", "onboarding.actions.back": "Ewch â fi yn ôl",
"onboarding.actions.go_to_explore": "Gweld beth yw'r tuedd", "onboarding.actions.go_to_explore": "Gweld beth yw'r tuedd",
@ -501,7 +504,7 @@
"privacy.change": "Addasu preifatrwdd y post", "privacy.change": "Addasu preifatrwdd y post",
"privacy.direct.long": "Dim ond yn weladwy i ddefnyddwyr a grybwyllwyd", "privacy.direct.long": "Dim ond yn weladwy i ddefnyddwyr a grybwyllwyd",
"privacy.direct.short": "Dim ond pobl sy wedi'u crybwyll", "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.private.short": "Dilynwyr yn unig",
"privacy.public.long": "Gweladwy i bawb", "privacy.public.long": "Gweladwy i bawb",
"privacy.public.short": "Cyhoeddus", "privacy.public.short": "Cyhoeddus",
@ -510,7 +513,7 @@
"privacy_policy.last_updated": "Diweddarwyd ddiwethaf ar {date}", "privacy_policy.last_updated": "Diweddarwyd ddiwethaf ar {date}",
"privacy_policy.title": "Polisi Preifatrwydd", "privacy_policy.title": "Polisi Preifatrwydd",
"refresh": "Adnewyddu", "refresh": "Adnewyddu",
"regeneration_indicator.label": "Llwytho…", "regeneration_indicator.label": "Yn llwytho…",
"regeneration_indicator.sublabel": "Mae eich ffrwd cartref yn cael ei baratoi!", "regeneration_indicator.sublabel": "Mae eich ffrwd cartref yn cael ei baratoi!",
"relative_time.days": "{number}d", "relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# diwrnod} other {# diwrnod}} yn ôl", "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.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.active_users": "defnyddwyr gweithredol",
"server_banner.administered_by": "Gweinyddir gan:", "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.learn_more": "Dysgu mwy",
"server_banner.server_stats": "Ystadegau'r gweinydd:", "server_banner.server_stats": "Ystadegau'r gweinydd:",
"sign_in_banner.create_account": "Creu cyfrif", "sign_in_banner.create_account": "Creu cyfrif",
"sign_in_banner.sign_in": "Mewngofnodi", "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.", "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_account": "Agor rhyngwyneb cymedroli ar gyfer @{name}",
"status.admin_domain": "Agor rhyngwyneb cymedroli {domain}", "status.admin_domain": "Agor rhyngwyneb cymedroli {domain}",
@ -635,7 +639,7 @@
"status.reblogged_by": "Hybodd {name}", "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.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.redraft": "Dileu ac ailddrafftio",
"status.remove_bookmark": "Dileu llyfrnod", "status.remove_bookmark": "Tynnu nod tudalen",
"status.replied_to": "Wedi ateb {name}", "status.replied_to": "Wedi ateb {name}",
"status.reply": "Ateb", "status.reply": "Ateb",
"status.replyAll": "Ateb i edefyn", "status.replyAll": "Ateb i edefyn",
@ -678,7 +682,7 @@
"units.short.thousand": "{count}mil", "units.short.thousand": "{count}mil",
"upload_area.title": "Llusgwch a gollwng i lwytho", "upload_area.title": "Llusgwch a gollwng i lwytho",
"upload_button.label": "Ychwanegwch gyfryngau (JPEG, PNG, GIF, WebM, MP4, MOV)", "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_error.poll": "Nid oes modd llwytho ffeiliau â phleidleisiau.",
"upload_form.audio_description": "Disgrifio ar gyfer pobl sydd â cholled clyw", "upload_form.audio_description": "Disgrifio ar gyfer pobl sydd â cholled clyw",
"upload_form.description": "Disgrifio i'r rheini a nam ar ei golwg", "upload_form.description": "Disgrifio i'r rheini a nam ar ei golwg",

View File

@ -191,7 +191,6 @@
"conversation.open": "Vis samtale", "conversation.open": "Vis samtale",
"conversation.with": "Med {names}", "conversation.with": "Med {names}",
"copypaste.copied": "Kopieret", "copypaste.copied": "Kopieret",
"copypaste.copy": "Kopiér",
"copypaste.copy_to_clipboard": "Kopiér til udklipsholder", "copypaste.copy_to_clipboard": "Kopiér til udklipsholder",
"directory.federated": "Fra kendt fedivers", "directory.federated": "Fra kendt fedivers",
"directory.local": "Kun fra {domain}", "directory.local": "Kun fra {domain}",
@ -296,6 +295,9 @@
"hashtag.column_settings.tag_mode.any": "Nogle af disse", "hashtag.column_settings.tag_mode.any": "Nogle af disse",
"hashtag.column_settings.tag_mode.none": "Ingen af disse", "hashtag.column_settings.tag_mode.none": "Ingen af disse",
"hashtag.column_settings.tag_toggle": "Inkludér ekstra tags for denne kolonne", "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.follow": "Følg hashtag",
"hashtag.unfollow": "Stop med at følge hashtag", "hashtag.unfollow": "Stop med at følge hashtag",
"home.actions.go_to_explore": "Se, hvad som trender", "home.actions.go_to_explore": "Se, hvad som trender",
@ -303,7 +305,7 @@
"home.column_settings.basic": "Grundlæggende", "home.column_settings.basic": "Grundlæggende",
"home.column_settings.show_reblogs": "Vis boosts", "home.column_settings.show_reblogs": "Vis boosts",
"home.column_settings.show_replies": "Vis svar", "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.explore_prompt.title": "Dette er din hjemmebase i Mastodon.",
"home.hide_announcements": "Skjul bekendtgørelser", "home.hide_announcements": "Skjul bekendtgørelser",
"home.show_announcements": "Vis 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.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.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.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_another_server": "På en anden server",
"interaction_modal.on_this_server": "På denne 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.sign_in": "Du er ikke logget ind på denne server. Hvor hostes din konto?",
"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_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.favourite": "Gør {name}s indlæg til favorit",
"interaction_modal.title.follow": "Følg {name}", "interaction_modal.title.follow": "Følg {name}",
"interaction_modal.title.reblog": "Boost {name}s indlæg", "interaction_modal.title.reblog": "Boost {name}s indlæg",
@ -596,6 +601,7 @@
"server_banner.server_stats": "Serverstatstik:", "server_banner.server_stats": "Serverstatstik:",
"sign_in_banner.create_account": "Opret konto", "sign_in_banner.create_account": "Opret konto",
"sign_in_banner.sign_in": "Log ind", "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.", "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_account": "Åbn modereringsbrugerflade for @{name}",
"status.admin_domain": "Åbn modereringsbrugerflade for {domain}", "status.admin_domain": "Åbn modereringsbrugerflade for {domain}",

View File

@ -150,7 +150,7 @@
"compose_form.poll.switch_to_multiple": "Mehrfachauswahl erlauben", "compose_form.poll.switch_to_multiple": "Mehrfachauswahl erlauben",
"compose_form.poll.switch_to_single": "Nur Einzelauswahl erlauben", "compose_form.poll.switch_to_single": "Nur Einzelauswahl erlauben",
"compose_form.publish": "Veröffentlichen", "compose_form.publish": "Veröffentlichen",
"compose_form.publish_form": "Veröffentlichen", "compose_form.publish_form": "Neuer Beitrag",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Änderungen speichern", "compose_form.save_changes": "Änderungen speichern",
"compose_form.sensitive.hide": "{count, plural, one {Mit einer Inhaltswarnung versehen} other {Mit einer Inhaltswarnung versehen}}", "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.open": "Unterhaltung anzeigen",
"conversation.with": "Mit {names}", "conversation.with": "Mit {names}",
"copypaste.copied": "Kopiert", "copypaste.copied": "Kopiert",
"copypaste.copy": "Kopieren",
"copypaste.copy_to_clipboard": "In die Zwischenablage kopieren", "copypaste.copy_to_clipboard": "In die Zwischenablage kopieren",
"directory.federated": "Aus bekanntem Fediverse", "directory.federated": "Aus bekanntem Fediverse",
"directory.local": "Nur von der Domain {domain}", "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.community_timeline": "Das sind die neuesten öffentlichen Beiträge von Profilen, deren Konten von {domain} verwaltet werden.",
"dismissable_banner.dismiss": "Ablehnen", "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_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.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.", "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.", "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.any": "Eines von diesen",
"hashtag.column_settings.tag_mode.none": "Keines von diesen", "hashtag.column_settings.tag_mode.none": "Keines von diesen",
"hashtag.column_settings.tag_toggle": "Zusätzliche Hashtags dieser Spalte hinzufügen", "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.follow": "Hashtag folgen",
"hashtag.unfollow": "Hashtag entfolgen", "hashtag.unfollow": "Hashtag entfolgen",
"home.actions.go_to_explore": "Trends ansehen", "home.actions.go_to_explore": "Trends ansehen",
@ -303,7 +305,7 @@
"home.column_settings.basic": "Einfach", "home.column_settings.basic": "Einfach",
"home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen", "home.column_settings.show_reblogs": "Geteilte Beiträge anzeigen",
"home.column_settings.show_replies": "Antworten 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.explore_prompt.title": "Das ist dein Zuhause bei Mastodon.",
"home.hide_announcements": "Ankündigungen ausblenden", "home.hide_announcements": "Ankündigungen ausblenden",
"home.show_announcements": "Ankündigungen anzeigen", "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.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.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.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_another_server": "Auf einem anderen Server",
"interaction_modal.on_this_server": "Auf diesem 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.sign_in": "Du bist auf diesem Server nicht angemeldet. Auf welchem Server wird dein Konto verwaltet?",
"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_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.favourite": "Beitrag von {name} favorisieren",
"interaction_modal.title.follow": "Folge {name}", "interaction_modal.title.follow": "Folge {name}",
"interaction_modal.title.reblog": "Beitrag von {name} teilen", "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.message": "Ich bin {username} auf #Mastodon! Folge mir auf {url}",
"onboarding.share.next_steps": "Mögliche nächste Schritte:", "onboarding.share.next_steps": "Mögliche nächste Schritte:",
"onboarding.share.title": "Teile dein Profil", "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.skip": "Du benötigst keine Hilfe für den Einstieg?",
"onboarding.start.title": "Du hast es geschafft!", "onboarding.start.title": "Du hast es geschafft!",
"onboarding.steps.follow_people.body": "Interessanten Profilen zu folgen ist das, was Mastodon ausmacht.", "onboarding.steps.follow_people.body": "Interessanten Profilen zu folgen ist das, was Mastodon ausmacht.",
@ -506,7 +511,7 @@
"privacy.private.short": "Nur Follower", "privacy.private.short": "Nur Follower",
"privacy.public.long": "Für alle sichtbar", "privacy.public.long": "Für alle sichtbar",
"privacy.public.short": "Öffentlich", "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.unlisted.short": "Nicht gelistet",
"privacy_policy.last_updated": "Stand: {date}", "privacy_policy.last_updated": "Stand: {date}",
"privacy_policy.title": "Datenschutzerklärung", "privacy_policy.title": "Datenschutzerklärung",
@ -526,7 +531,7 @@
"relative_time.today": "heute", "relative_time.today": "heute",
"reply_indicator.cancel": "Abbrechen", "reply_indicator.cancel": "Abbrechen",
"report.block": "Blockieren", "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.other": "Andere",
"report.categories.spam": "Spam", "report.categories.spam": "Spam",
"report.categories.violation": "Der Inhalt verletzt eine oder mehrere Serverregeln", "report.categories.violation": "Der Inhalt verletzt eine oder mehrere Serverregeln",
@ -539,13 +544,13 @@
"report.forward": "Meldung zusätzlich an {target} weiterleiten", "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.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": "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.next": "Weiter",
"report.placeholder": "Ergänzende Hinweise", "report.placeholder": "Ergänzende Hinweise",
"report.reasons.dislike": "Das gefällt mir nicht", "report.reasons.dislike": "Das gefällt mir nicht",
"report.reasons.dislike_description": "Das ist etwas, das du nicht sehen möchtest", "report.reasons.dislike_description": "Das ist etwas, das du nicht sehen möchtest",
"report.reasons.legal": "Das ist illegal", "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": "Es ist etwas anderes",
"report.reasons.other_description": "Der Vorfall passt zu keiner dieser Kategorien", "report.reasons.other_description": "Der Vorfall passt zu keiner dieser Kategorien",
"report.reasons.spam": "Das ist Spam", "report.reasons.spam": "Das ist Spam",
@ -555,8 +560,8 @@
"report.rules.subtitle": "Wähle alle zutreffenden Inhalte aus", "report.rules.subtitle": "Wähle alle zutreffenden Inhalte aus",
"report.rules.title": "Welche Regeln werden verletzt?", "report.rules.title": "Welche Regeln werden verletzt?",
"report.statuses.subtitle": "Wähle alle zutreffenden Inhalte aus", "report.statuses.subtitle": "Wähle alle zutreffenden Inhalte aus",
"report.statuses.title": "Gibt es Beiträge, die diesen Bericht untermauern?", "report.statuses.title": "Gibt es Beiträge, die diese Meldung bekräftigen?",
"report.submit": "Absenden", "report.submit": "Senden",
"report.target": "{target} melden", "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": "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:", "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.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.active_users": "aktive Profile",
"server_banner.administered_by": "Verwaltet von:", "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.learn_more": "Mehr erfahren",
"server_banner.server_stats": "Serverstatistiken:", "server_banner.server_stats": "Serverstatistiken:",
"sign_in_banner.create_account": "Konto erstellen", "sign_in_banner.create_account": "Konto erstellen",
"sign_in_banner.sign_in": "Anmelden", "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.", "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_account": "@{name} moderieren",
"status.admin_domain": "{domain} moderieren", "status.admin_domain": "{domain} moderieren",

View File

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

View File

@ -13,14 +13,14 @@
"about.rules": "Server rules", "about.rules": "Server rules",
"account.account_note_header": "Note", "account.account_note_header": "Note",
"account.add_or_remove_from_list": "Add or Remove from lists", "account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot", "account.badges.bot": "Automated",
"account.badges.group": "Group", "account.badges.group": "Group",
"account.block": "Block @{name}", "account.block": "Block @{name}",
"account.block_domain": "Unblock domain {domain}", "account.block_domain": "Block domain {domain}",
"account.block_short": "Block", "account.block_short": "Block",
"account.blocked": "Blocked", "account.blocked": "Blocked",
"account.browse_more_on_origin_server": "Browse more on the original profile", "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.direct": "Privately mention @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts", "account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocked": "Domain blocked", "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_multiple": "Change poll to allow multiple choices",
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice", "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.publish": "Publish", "compose_form.publish": "Publish",
"compose_form.publish_form": "Publish", "compose_form.publish_form": "New post",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "Save changes", "compose_form.save_changes": "Save changes",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}", "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.open": "View conversation",
"conversation.with": "With {names}", "conversation.with": "With {names}",
"copypaste.copied": "Copied", "copypaste.copied": "Copied",
"copypaste.copy": "Copy",
"copypaste.copy_to_clipboard": "Copy to clipboard", "copypaste.copy_to_clipboard": "Copy to clipboard",
"directory.federated": "From known fediverse", "directory.federated": "From known fediverse",
"directory.local": "From {domain} only", "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.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.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.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.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.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.", "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.any": "Any of these",
"hashtag.column_settings.tag_mode.none": "None of these", "hashtag.column_settings.tag_mode.none": "None of these",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column", "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.follow": "Follow hashtag",
"hashtag.unfollow": "Unfollow hashtag", "hashtag.unfollow": "Unfollow hashtag",
"home.actions.go_to_explore": "See what's trending", "home.actions.go_to_explore": "See what's trending",
@ -303,7 +303,7 @@
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts", "home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies", "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.explore_prompt.title": "This is your home base within Mastodon.",
"home.hide_announcements": "Hide announcements", "home.hide_announcements": "Hide announcements",
"home.show_announcements": "Show 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.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.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.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_another_server": "On a different server",
"interaction_modal.on_this_server": "On this 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.sign_in": "You are not logged in to this server. Where is your account hosted?",
"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_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.favourite": "Favourite {name}'s post",
"interaction_modal.title.follow": "Follow {name}", "interaction_modal.title.follow": "Follow {name}",
"interaction_modal.title.reblog": "Boost {name}'s post", "interaction_modal.title.reblog": "Boost {name}'s post",
@ -335,14 +338,14 @@
"keyboard_shortcuts.favourites": "Open favourites list", "keyboard_shortcuts.favourites": "Open favourites list",
"keyboard_shortcuts.federated": "to open federated timeline", "keyboard_shortcuts.federated": "to open federated timeline",
"keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "to open home timeline", "keyboard_shortcuts.home": "Open home timeline",
"keyboard_shortcuts.hotkey": "Hotkey", "keyboard_shortcuts.hotkey": "Hotkey",
"keyboard_shortcuts.legend": "to display this legend", "keyboard_shortcuts.legend": "to display this legend",
"keyboard_shortcuts.local": "to open local timeline", "keyboard_shortcuts.local": "to open local timeline",
"keyboard_shortcuts.mention": "to mention author", "keyboard_shortcuts.mention": "to mention author",
"keyboard_shortcuts.muted": "to open muted users list", "keyboard_shortcuts.muted": "to open muted users list",
"keyboard_shortcuts.my_profile": "to open your profile", "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.open_media": "to open media",
"keyboard_shortcuts.pinned": "to open pinned posts list", "keyboard_shortcuts.pinned": "to open pinned posts list",
"keyboard_shortcuts.profile": "to open author's profile", "keyboard_shortcuts.profile": "to open author's profile",
@ -352,10 +355,10 @@
"keyboard_shortcuts.spoilers": "to show/hide CW field", "keyboard_shortcuts.spoilers": "to show/hide CW field",
"keyboard_shortcuts.start": "to open \"get started\" column", "keyboard_shortcuts.start": "to open \"get started\" column",
"keyboard_shortcuts.toggle_hidden": "to show/hide text behind CW", "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.toot": "to start a brand new post",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "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.close": "Close",
"lightbox.compress": "Compress image view box", "lightbox.compress": "Compress image view box",
"lightbox.expand": "Expand image view box", "lightbox.expand": "Expand image view box",
@ -393,7 +396,7 @@
"navigation_bar.compose": "Compose new post", "navigation_bar.compose": "Compose new post",
"navigation_bar.direct": "Private mentions", "navigation_bar.direct": "Private mentions",
"navigation_bar.discover": "Discover", "navigation_bar.discover": "Discover",
"navigation_bar.domain_blocks": "Hidden domains", "navigation_bar.domain_blocks": "Blocked domains",
"navigation_bar.edit_profile": "Edit profile", "navigation_bar.edit_profile": "Edit profile",
"navigation_bar.explore": "Explore", "navigation_bar.explore": "Explore",
"navigation_bar.favourites": "Favourites", "navigation_bar.favourites": "Favourites",
@ -462,26 +465,26 @@
"onboarding.action.back": "Take me back", "onboarding.action.back": "Take me back",
"onboarding.actions.back": "Take me back", "onboarding.actions.back": "Take me back",
"onboarding.actions.go_to_explore": "See what's trending", "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.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.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.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.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.message": "I'm {username} on #Mastodon! Come follow me at {url}",
"onboarding.share.next_steps": "Possible next steps:", "onboarding.share.next_steps": "Possible next steps:",
"onboarding.share.title": "Share your profile", "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.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": "Want to skip right ahead?", "onboarding.start.skip": "Don't need help getting started?",
"onboarding.start.title": "You've made it!", "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.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.follow_people.title": "Personalize your home feed",
"onboarding.steps.publish_status.body": "Say hello to the World.", "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.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.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "Customise your 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.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.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.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!", "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.voted": "You voted for this answer",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}", "poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll_button.add_poll": "Add a poll", "poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Add a poll", "poll_button.remove_poll": "Remove poll",
"privacy.change": "Adjust status privacy", "privacy.change": "Change post privacy",
"privacy.direct.long": "Visible for mentioned users only", "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.long": "Visible for followers only",
"privacy.private.short": "Followers-only", "privacy.private.short": "Followers-only",
"privacy.public.long": "Visible for all", "privacy.public.long": "Visible for all",
@ -541,7 +544,7 @@
"report.mute": "Mute", "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.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.next": "Next",
"report.placeholder": "Type or paste additional comments", "report.placeholder": "Additional comments",
"report.reasons.dislike": "I don't like it", "report.reasons.dislike": "I don't like it",
"report.reasons.dislike_description": "It is not something you want to see", "report.reasons.dislike_description": "It is not something you want to see",
"report.reasons.legal": "It's illegal", "report.reasons.legal": "It's illegal",
@ -556,8 +559,8 @@
"report.rules.title": "Which rules are being violated?", "report.rules.title": "Which rules are being violated?",
"report.statuses.subtitle": "Select all that apply", "report.statuses.subtitle": "Select all that apply",
"report.statuses.title": "Are there any posts that back up this report?", "report.statuses.title": "Are there any posts that back up this report?",
"report.submit": "Submit report", "report.submit": "Submit",
"report.target": "Report {target}", "report.target": "Reporting {target}",
"report.thanks.take_action": "Here are your options for controlling what you see on Mastodon:", "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.take_action_actionable": "While we review this, you can take action against @{name}:",
"report.thanks.title": "Don't want to see this?", "report.thanks.title": "Don't want to see this?",
@ -596,10 +599,11 @@
"server_banner.server_stats": "Server stats:", "server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account", "sign_in_banner.create_account": "Create account",
"sign_in_banner.sign_in": "Sign in", "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.", "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_account": "Open moderation interface for @{name}",
"status.admin_domain": "Open moderation interface for {domain}", "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.block": "Block @{name}",
"status.bookmark": "Bookmark", "status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost", "status.cancel_reblog_private": "Unboost",
@ -627,7 +631,7 @@
"status.more": "More", "status.more": "More",
"status.mute": "Mute @{name}", "status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation", "status.mute_conversation": "Mute conversation",
"status.open": "Expand this status", "status.open": "Expand this post",
"status.pin": "Pin on profile", "status.pin": "Pin on profile",
"status.pinned": "Pinned post", "status.pinned": "Pinned post",
"status.read_more": "Read more", "status.read_more": "Read more",
@ -681,13 +685,13 @@
"upload_button.label": "Add images, a video or an audio file", "upload_button.label": "Add images, a video or an audio file",
"upload_error.limit": "File upload limit exceeded.", "upload_error.limit": "File upload limit exceeded.",
"upload_error.poll": "File upload not allowed with polls.", "upload_error.poll": "File upload not allowed with polls.",
"upload_form.audio_description": "Describe for people with hearing loss", "upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
"upload_form.description": "Describe for the visually impaired", "upload_form.description": "Describe for people who are blind or have low vision",
"upload_form.description_missing": "No description added", "upload_form.description_missing": "No description added",
"upload_form.edit": "Edit", "upload_form.edit": "Edit",
"upload_form.thumbnail": "Change thumbnail", "upload_form.thumbnail": "Change thumbnail",
"upload_form.undo": "Delete", "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.analyzing_picture": "Analysing picture…",
"upload_modal.apply": "Apply", "upload_modal.apply": "Apply",
"upload_modal.applying": "Applying…", "upload_modal.applying": "Applying…",
@ -700,7 +704,7 @@
"upload_modal.preview_label": "Preview ({ratio})", "upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Uploading…", "upload_progress.label": "Uploading…",
"upload_progress.processing": "Processing…", "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.close": "Close video",
"video.download": "Download file", "video.download": "Download file",
"video.exit_fullscreen": "Exit full screen", "video.exit_fullscreen": "Exit full screen",

View File

@ -295,8 +295,12 @@
"hashtag.column_settings.tag_mode.any": "Any of these", "hashtag.column_settings.tag_mode.any": "Any of these",
"hashtag.column_settings.tag_mode.none": "None of these", "hashtag.column_settings.tag_mode.none": "None of these",
"hashtag.column_settings.tag_toggle": "Include additional tags for this column", "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.follow": "Follow hashtag",
"hashtag.unfollow": "Unfollow 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_explore": "See what's trending",
"home.actions.go_to_suggestions": "Find people to follow", "home.actions.go_to_suggestions": "Find people to follow",
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Basic",
@ -529,6 +533,7 @@
"reply_indicator.cancel": "Cancel", "reply_indicator.cancel": "Cancel",
"report.block": "Block", "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.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.other": "Other",
"report.categories.spam": "Spam", "report.categories.spam": "Spam",
"report.categories.violation": "Content violates one or more server rules", "report.categories.violation": "Content violates one or more server rules",
@ -598,6 +603,7 @@
"server_banner.server_stats": "Server stats:", "server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account", "sign_in_banner.create_account": "Create account",
"sign_in_banner.sign_in": "Login", "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.", "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_account": "Open moderation interface for @{name}",
"status.admin_domain": "Open moderation interface for {domain}", "status.admin_domain": "Open moderation interface for {domain}",

View File

@ -189,7 +189,6 @@
"conversation.open": "Vidi konversacion", "conversation.open": "Vidi konversacion",
"conversation.with": "Kun {names}", "conversation.with": "Kun {names}",
"copypaste.copied": "Kopiita", "copypaste.copied": "Kopiita",
"copypaste.copy": "Kopii",
"copypaste.copy_to_clipboard": "Kopii al dosierujo", "copypaste.copy_to_clipboard": "Kopii al dosierujo",
"directory.federated": "El konata fediverso", "directory.federated": "El konata fediverso",
"directory.local": "Nur de {domain}", "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.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_another_server": "En alia servilo",
"interaction_modal.on_this_server": "En ĉi tiu 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.follow": "Sekvi {name}",
"interaction_modal.title.reblog": "Akceli la afiŝon de {name}", "interaction_modal.title.reblog": "Akceli la afiŝon de {name}",
"interaction_modal.title.reply": "Respondi al la afiŝo de {name}", "interaction_modal.title.reply": "Respondi al la afiŝo de {name}",

View File

@ -191,7 +191,6 @@
"conversation.open": "Ver conversación", "conversation.open": "Ver conversación",
"conversation.with": "Con {names}", "conversation.with": "Con {names}",
"copypaste.copied": "Copiado", "copypaste.copied": "Copiado",
"copypaste.copy": "Copiar",
"copypaste.copy_to_clipboard": "Copiar al portapapeles", "copypaste.copy_to_clipboard": "Copiar al portapapeles",
"directory.federated": "Desde fediverso conocido", "directory.federated": "Desde fediverso conocido",
"directory.local": "Sólo de {domain}", "directory.local": "Sólo de {domain}",
@ -296,6 +295,9 @@
"hashtag.column_settings.tag_mode.any": "Cualquiera de estas", "hashtag.column_settings.tag_mode.any": "Cualquiera de estas",
"hashtag.column_settings.tag_mode.none": "Ninguna de estas", "hashtag.column_settings.tag_mode.none": "Ninguna de estas",
"hashtag.column_settings.tag_toggle": "Incluir etiquetas adicionales para esta columna", "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.follow": "Seguir etiqueta",
"hashtag.unfollow": "Dejar de seguir etiqueta", "hashtag.unfollow": "Dejar de seguir etiqueta",
"home.actions.go_to_explore": "Mirá qué está en tendencia", "home.actions.go_to_explore": "Mirá qué está en tendencia",
@ -303,7 +305,7 @@
"home.column_settings.basic": "Básico", "home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar adhesiones", "home.column_settings.show_reblogs": "Mostrar adhesiones",
"home.column_settings.show_replies": "Mostrar respuestas", "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.explore_prompt.title": "Este es tu inicio en Mastodon.",
"home.hide_announcements": "Ocultar anuncios", "home.hide_announcements": "Ocultar anuncios",
"home.show_announcements": "Mostrar 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.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.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.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_another_server": "En un servidor diferente",
"interaction_modal.on_this_server": "En este servidor", "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.sign_in": "No iniciaste sesión en este servidor. ¿Dónde se encuentra alojada tu cuenta?",
"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_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.favourite": "Marcar como favorito el mensaje de {name}",
"interaction_modal.title.follow": "Seguir a {name}", "interaction_modal.title.follow": "Seguir a {name}",
"interaction_modal.title.reblog": "Adherir al mensaje de {name}", "interaction_modal.title.reblog": "Adherir al mensaje de {name}",
@ -596,6 +601,7 @@
"server_banner.server_stats": "Estadísticas del servidor:", "server_banner.server_stats": "Estadísticas del servidor:",
"sign_in_banner.create_account": "Crear cuenta", "sign_in_banner.create_account": "Crear cuenta",
"sign_in_banner.sign_in": "Iniciar sesión", "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.", "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_account": "Abrir interface de moderación para @{name}",
"status.admin_domain": "Abrir interface de moderación para {domain}", "status.admin_domain": "Abrir interface de moderación para {domain}",
@ -671,7 +677,7 @@
"timeline_hint.resources.followers": "Tus seguidores", "timeline_hint.resources.followers": "Tus seguidores",
"timeline_hint.resources.follows": "Las cuentas que seguís", "timeline_hint.resources.follows": "Las cuentas que seguís",
"timeline_hint.resources.statuses": "Mensajes más antiguos", "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", "trends.trending_now": "Tendencia ahora",
"ui.beforeunload": "Tu borrador se perderá si abandonás Mastodon.", "ui.beforeunload": "Tu borrador se perderá si abandonás Mastodon.",
"units.short.billion": "{count}MM", "units.short.billion": "{count}MM",

View File

@ -191,7 +191,6 @@
"conversation.open": "Ver conversación", "conversation.open": "Ver conversación",
"conversation.with": "Con {names}", "conversation.with": "Con {names}",
"copypaste.copied": "Copiado", "copypaste.copied": "Copiado",
"copypaste.copy": "Copiar",
"copypaste.copy_to_clipboard": "Copiar al portapapeles", "copypaste.copy_to_clipboard": "Copiar al portapapeles",
"directory.federated": "Desde el fediverso conocido", "directory.federated": "Desde el fediverso conocido",
"directory.local": "Sólo de {domain}", "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.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.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_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.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}.", "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.", "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.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.domain_blocks": "Todavía no hay dominios ocultos.",
"empty_column.explore_statuses": "Nada es tendencia en este momento. ¡Revisa más tarde!", "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.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.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.", "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.any": "Cualquiera de estos",
"hashtag.column_settings.tag_mode.none": "Ninguno de estos", "hashtag.column_settings.tag_mode.none": "Ninguno de estos",
"hashtag.column_settings.tag_toggle": "Incluye etiquetas adicionales para esta columna", "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.follow": "Seguir etiqueta",
"hashtag.unfollow": "Dejar de seguir etiqueta", "hashtag.unfollow": "Dejar de seguir etiqueta",
"home.actions.go_to_explore": "Ver tendencias", "home.actions.go_to_explore": "Ver tendencias",
@ -300,16 +305,22 @@
"home.column_settings.basic": "Básico", "home.column_settings.basic": "Básico",
"home.column_settings.show_reblogs": "Mostrar retoots", "home.column_settings.show_reblogs": "Mostrar retoots",
"home.column_settings.show_replies": "Mostrar respuestas", "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.explore_prompt.title": "Este es tu punto de partida en Mastodon.",
"home.hide_announcements": "Ocultar anuncios", "home.hide_announcements": "Ocultar anuncios",
"home.show_announcements": "Mostrar 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.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.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.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_another_server": "En un servidor diferente",
"interaction_modal.on_this_server": "En este servidor", "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.follow": "Seguir a {name}",
"interaction_modal.title.reblog": "Impulsar la publicación de {name}", "interaction_modal.title.reblog": "Impulsar la publicación de {name}",
"interaction_modal.title.reply": "Responder 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.direct": "para abrir la columna de menciones privadas",
"keyboard_shortcuts.down": "mover hacia abajo en la lista", "keyboard_shortcuts.down": "mover hacia abajo en la lista",
"keyboard_shortcuts.enter": "abrir estado", "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.federated": "abrir el timeline federado",
"keyboard_shortcuts.heading": "Keyboard Shortcuts", "keyboard_shortcuts.heading": "Keyboard Shortcuts",
"keyboard_shortcuts.home": "abrir el timeline propio", "keyboard_shortcuts.home": "abrir el timeline propio",
@ -355,6 +368,7 @@
"lightbox.previous": "Anterior", "lightbox.previous": "Anterior",
"limited_account_hint.action": "Mostrar perfil de todos modos", "limited_account_hint.action": "Mostrar perfil de todos modos",
"limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.", "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.add": "Añadir a lista",
"lists.account.remove": "Quitar de lista", "lists.account.remove": "Quitar de lista",
"lists.delete": "Borrar lista", "lists.delete": "Borrar lista",
@ -387,6 +401,7 @@
"navigation_bar.domain_blocks": "Dominios ocultos", "navigation_bar.domain_blocks": "Dominios ocultos",
"navigation_bar.edit_profile": "Editar perfil", "navigation_bar.edit_profile": "Editar perfil",
"navigation_bar.explore": "Explorar", "navigation_bar.explore": "Explorar",
"navigation_bar.favourites": "Favoritos",
"navigation_bar.filters": "Palabras silenciadas", "navigation_bar.filters": "Palabras silenciadas",
"navigation_bar.follow_requests": "Solicitudes para seguirte", "navigation_bar.follow_requests": "Solicitudes para seguirte",
"navigation_bar.followed_tags": "Hashtags seguidos", "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.", "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.report": "{name} denunció a {target}",
"notification.admin.sign_up": "{name} se unio", "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": "{name} te empezó a seguir",
"notification.follow_request": "{name} ha solicitado seguirte", "notification.follow_request": "{name} ha solicitado seguirte",
"notification.mention": "{name} te ha mencionado", "notification.mention": "{name} te ha mencionado",
@ -416,6 +432,7 @@
"notifications.column_settings.admin.report": "Nuevas denuncias:", "notifications.column_settings.admin.report": "Nuevas denuncias:",
"notifications.column_settings.admin.sign_up": "Registros nuevos:", "notifications.column_settings.admin.sign_up": "Registros nuevos:",
"notifications.column_settings.alert": "Notificaciones de escritorio", "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.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido", "notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros", "notifications.column_settings.filter_bar.show_bar": "Mostrar barra de filtros",
@ -433,6 +450,7 @@
"notifications.column_settings.update": "Ediciones:", "notifications.column_settings.update": "Ediciones:",
"notifications.filter.all": "Todos", "notifications.filter.all": "Todos",
"notifications.filter.boosts": "Retoots", "notifications.filter.boosts": "Retoots",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguidores", "notifications.filter.follows": "Seguidores",
"notifications.filter.mentions": "Menciones", "notifications.filter.mentions": "Menciones",
"notifications.filter.polls": "Resultados de la votación", "notifications.filter.polls": "Resultados de la votación",
@ -583,6 +601,8 @@
"server_banner.server_stats": "Estadísticas del servidor:", "server_banner.server_stats": "Estadísticas del servidor:",
"sign_in_banner.create_account": "Crear cuenta", "sign_in_banner.create_account": "Crear cuenta",
"sign_in_banner.sign_in": "Iniciar sesión", "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_account": "Abrir interfaz de moderación para @{name}",
"status.admin_domain": "Abrir interfaz de moderación para {domain}", "status.admin_domain": "Abrir interfaz de moderación para {domain}",
"status.admin_status": "Abrir este estado en la interfaz de moderación", "status.admin_status": "Abrir este estado en la interfaz de moderación",
@ -599,6 +619,7 @@
"status.edited": "Editado {date}", "status.edited": "Editado {date}",
"status.edited_x_times": "Editado {count, plural, one {{count} time} other {{count} veces}}", "status.edited_x_times": "Editado {count, plural, one {{count} time} other {{count} veces}}",
"status.embed": "Incrustado", "status.embed": "Incrustado",
"status.favourite": "Favorito",
"status.filter": "Filtrar esta publicación", "status.filter": "Filtrar esta publicación",
"status.filtered": "Filtrado", "status.filtered": "Filtrado",
"status.hide": "Ocultar toot", "status.hide": "Ocultar toot",

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