Merge branch 'main' into bark-prod
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dalite 2024-04-26 13:39:14 +02:00
commit 065aa1a32c
484 changed files with 6645 additions and 3176 deletions

4
.env.development Normal file
View file

@ -0,0 +1,4 @@
# Required by ActiveRecord encryption feature
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr

View file

@ -3,3 +3,8 @@ NODE_ENV=production
# Federation # Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true LOCAL_HTTPS=true
# Required by ActiveRecord encryption feature
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr

View file

@ -363,6 +363,7 @@ module.exports = defineConfig({
"message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead." "message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
} }
], ],
"@typescript-eslint/restrict-template-expressions": ['warn', { allowNumber: true }],
'jsdoc/require-jsdoc': 'off', 'jsdoc/require-jsdoc': 'off',
// Those rules set stricter rules for TS files // Those rules set stricter rules for TS files

View file

@ -52,7 +52,7 @@ jobs:
# Create or update the pull request # Create or update the pull request
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v6.0.2 uses: peter-evans/create-pull-request@v6.0.4
with: with:
commit-message: 'New Crowdin translations' commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)' title: 'New Crowdin Translations (automated)'

View file

@ -38,5 +38,5 @@ jobs:
- name: Set up Javascript environment - name: Set up Javascript environment
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript
- name: Jest testing - name: JavaScript testing
run: yarn jest --reporters github-actions summary run: yarn jest --reporters github-actions summary

View file

@ -28,6 +28,9 @@ jobs:
env: env:
RAILS_ENV: ${{ matrix.mode }} RAILS_ENV: ${{ matrix.mode }}
BUNDLE_WITH: ${{ matrix.mode }} BUNDLE_WITH: ${{ matrix.mode }}
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: precompile_placeholder
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: precompile_placeholder
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: precompile_placeholder
OTP_SECRET: precompile_placeholder OTP_SECRET: precompile_placeholder
SECRET_KEY_BASE: precompile_placeholder SECRET_KEY_BASE: precompile_placeholder
@ -111,7 +114,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
ruby-version: ruby-version:
- '3.0'
- '3.1' - '3.1'
- '.ruby-version' - '.ruby-version'
- '3.3' - '3.3'
@ -187,7 +189,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
ruby-version: ruby-version:
- '3.0'
- '3.1' - '3.1'
- '.ruby-version' - '.ruby-version'
- '3.3' - '3.3'
@ -287,7 +288,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
ruby-version: ruby-version:
- '3.0'
- '3.1' - '3.1'
- '.ruby-version' - '.ruby-version'
- '3.3' - '3.3'

1
.gitignore vendored
View file

@ -24,7 +24,6 @@
/public/packs-test /public/packs-test
.env .env
.env.production .env.production
.env.development
/node_modules/ /node_modules/
/build/ /build/

View file

@ -1,6 +1,5 @@
exclude: exclude:
- 'vendor/**/*' - 'vendor/**/*'
- lib/templates/haml/scaffold/_form.html.haml
require: require:
- ./lib/linter/haml_middle_dot.rb - ./lib/linter/haml_middle_dot.rb
@ -11,6 +10,6 @@ linters:
MiddleDot: MiddleDot:
enabled: true enabled: true
LineLength: LineLength:
max: 320 max: 300
ViewLength: ViewLength:
max: 200 # Override default value of 100 inherited from rubocop max: 200 # Override default value of 100 inherited from rubocop

2
.nvmrc
View file

@ -1 +1 @@
20.11 20.12

View file

@ -9,12 +9,13 @@ inherit_mode:
require: require:
- rubocop-rails - rubocop-rails
- rubocop-rspec - rubocop-rspec
- rubocop-rspec_rails
- rubocop-performance - rubocop-performance
- rubocop-capybara - rubocop-capybara
- ./lib/linter/rubocop_middle_dot - ./lib/linter/rubocop_middle_dot
AllCops: AllCops:
TargetRubyVersion: 3.0 # Set to minimum supported version of CI TargetRubyVersion: 3.1 # Set to minimum supported version of CI
DisplayCopNames: true DisplayCopNames: true
DisplayStyleGuide: true DisplayStyleGuide: true
ExtraDetails: true ExtraDetails: true
@ -39,13 +40,7 @@ Layout/FirstHashElementIndentation:
# Reason: Currently disabled in .rubocop_todo.yml # Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength # https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
Layout/LineLength: Layout/LineLength:
Max: 320 # Default of 120 causes a duplicate entry in generated todo file Max: 300 # Default of 120 causes a duplicate entry in generated todo file
# Reason:
# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier
Lint/UselessAccessModifier:
ContextCreatingMethods:
- class_methods
## Disable most Metrics/*Length cops ## Disable most Metrics/*Length cops
# Reason: those are often triggered and force significant refactors when this happend # Reason: those are often triggered and force significant refactors when this happend
@ -86,6 +81,11 @@ Metrics/CyclomaticComplexity:
Metrics/ParameterLists: Metrics/ParameterLists:
CountKeywordArgs: false CountKeywordArgs: false
# Reason: Prefer seeing a variable name
# https://docs.rubocop.org/rubocop/cops_naming.html#namingblockforwarding
Naming/BlockForwarding:
EnforcedStyle: explicit
# Reason: Prevailing style is argument file paths # Reason: Prevailing style is argument file paths
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
Rails/FilePath: Rails/FilePath:
@ -148,11 +148,6 @@ RSpec/NamedSubject:
RSpec/NotToNot: RSpec/NotToNot:
EnforcedStyle: to_not EnforcedStyle: to_not
# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus
# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus
RSpec/Rails/HttpStatus:
EnforcedStyle: numeric
# Reason: Match overrides from Rspec/FilePath rule above # Reason: Match overrides from Rspec/FilePath rule above
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat
RSpec/SpecFilePathFormat: RSpec/SpecFilePathFormat:
@ -163,6 +158,11 @@ RSpec/SpecFilePathFormat:
OEmbedController: oembed_controller OEmbedController: oembed_controller
OStatus: ostatus OStatus: ostatus
# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus
# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus
RSpecRails/HttpStatus:
EnforcedStyle: numeric
# Reason: # Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren # https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
@ -182,10 +182,16 @@ Style/FormatStringToken:
AllowedMethods: AllowedMethods:
- redirect_with_vary - redirect_with_vary
# Reason: Prevailing style choice
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashaslastarrayitem
Style/HashAsLastArrayItem:
Enabled: false
# Reason: Enforce modern Ruby style # Reason: Enforce modern Ruby style
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax # https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax
Style/HashSyntax: Style/HashSyntax:
EnforcedStyle: ruby19_no_mixed_keys EnforcedStyle: ruby19_no_mixed_keys
EnforcedShorthandSyntax: either
# Reason: # Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals # https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals

View file

@ -1,18 +1,11 @@
# 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.60.2. # using RuboCop version 1.62.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
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
# Include: **/*.gemfile, **/Gemfile, **/gems.rb
Bundler/OrderedGems:
Exclude:
- 'Gemfile'
Lint/NonLocalExitFromIterator: Lint/NonLocalExitFromIterator:
Exclude: Exclude:
- 'app/helpers/jsonld_helper.rb' - 'app/helpers/jsonld_helper.rb'
@ -36,7 +29,7 @@ Metrics/PerceivedComplexity:
# Configuration parameters: CountAsOne. # Configuration parameters: CountAsOne.
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 20 # Override default of 5 Max: 18
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 7 Max: 7
@ -61,15 +54,6 @@ Rails/OutputSafety:
Exclude: Exclude:
- 'config/initializers/simple_form.rb' - 'config/initializers/simple_form.rb'
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/UniqueValidationWithoutIndex:
Exclude:
- 'app/models/account_alias.rb'
- 'app/models/custom_filter_status.rb'
- 'app/models/identity.rb'
- 'app/models/webauthn_credential.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
# AllowedMethods: ==, equal?, eql? # AllowedMethods: ==, equal?, eql?
@ -88,7 +72,6 @@ Style/FetchEnvVar:
Exclude: Exclude:
- 'app/lib/redis_configuration.rb' - 'app/lib/redis_configuration.rb'
- 'app/lib/translation_service.rb' - 'app/lib/translation_service.rb'
- 'config/environments/development.rb'
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/2_limited_federation_mode.rb'
- 'config/initializers/3_omniauth.rb' - 'config/initializers/3_omniauth.rb'
@ -98,7 +81,6 @@ Style/FetchEnvVar:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/initializers/vapid.rb' - 'config/initializers/vapid.rb'
- 'lib/mastodon/redis_config.rb' - 'lib/mastodon/redis_config.rb'
- 'lib/premailer_webpack_strategy.rb'
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
- 'spec/features/profile_spec.rb' - 'spec/features/profile_spec.rb'
@ -144,22 +126,8 @@ Style/GuardClause:
- 'lib/mastodon/cli/accounts.rb' - 'lib/mastodon/cli/accounts.rb'
- 'lib/mastodon/cli/maintenance.rb' - 'lib/mastodon/cli/maintenance.rb'
- 'lib/mastodon/cli/media.rb' - 'lib/mastodon/cli/media.rb'
- 'lib/paperclip/attachment_extensions.rb'
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: braces, no_braces
Style/HashAsLastArrayItem:
Exclude:
- 'app/controllers/admin/statuses_controller.rb'
- 'app/controllers/api/v1/statuses_controller.rb'
- 'app/models/concerns/account/counters.rb'
- 'app/models/concerns/status/threading_concern.rb'
- 'app/models/status.rb'
- 'app/services/batched_remove_status_service.rb'
- 'app/services/notify_service.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Style/HashTransformValues: Style/HashTransformValues:
Exclude: Exclude:
@ -207,13 +175,6 @@ Style/OptionalBooleanParameter:
- 'app/workers/unfollow_follow_worker.rb' - 'app/workers/unfollow_follow_worker.rb'
- 'lib/mastodon/redis_config.rb' - 'lib/mastodon/redis_config.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'config/deploy.rb'
- 'config/initializers/doorkeeper.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: short, verbose # SupportedStyles: short, verbose
@ -252,44 +213,12 @@ Style/SignalException:
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb' - 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/SingleArgumentDig:
Exclude:
- 'lib/webpacker/manifest_extensions.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Mode. # Configuration parameters: Mode.
Style/StringConcatenation: Style/StringConcatenation:
Exclude: Exclude:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiterals:
Exclude:
- 'config/environments/production.rb'
- 'config/initializers/backtrace_silencers.rb'
- 'config/initializers/http_client_proxy.rb'
- 'config/initializers/rack_attack.rb'
- 'config/initializers/webauthn.rb'
- 'config/routes.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArguments:
Exclude:
- 'config/initializers/paperclip.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInHashLiteral:
Exclude:
- 'config/environments/production.rb'
- 'config/environments/test.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: WordRegex. # Configuration parameters: WordRegex.
# SupportedStyles: percent, brackets # SupportedStyles: percent, brackets

View file

@ -1 +1 @@
3.2.3 3.2.4

View file

@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.4 # syntax=docker/dockerfile:1.7
# Please see https://docs.docker.com/engine/reference/builder for information about # Please see https://docs.docker.com/engine/reference/builder for information about
# the extended buildx capabilities used in this file. # the extended buildx capabilities used in this file.
@ -7,15 +7,15 @@
ARG TARGETPLATFORM=${TARGETPLATFORM} ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM}
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.3"] # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.4"]
ARG RUBY_VERSION="3.2.3" ARG RUBY_VERSION="3.2.4"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
ARG NODE_MAJOR_VERSION="20" ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm" ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) # Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
# Ruby image to use for base image based on combined variables (ex: 3.2.3-slim-bookworm) # Ruby image to use for base image based on combined variables (ex: 3.2.4-slim-bookworm)
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA="prod"
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files # See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
ARG RAILS_SERVE_STATIC_FILES="true" ARG RAILS_SERVE_STATIC_FILES="true"
# Allow to use YJIT compiler # Allow to use YJIT compiler
# See: https://github.com/ruby/ruby/blob/v3_2_3/doc/yjit/yjit.md # See: https://github.com/ruby/ruby/blob/v3_2_4/doc/yjit/yjit.md
ARG RUBY_YJIT_ENABLE="1" ARG RUBY_YJIT_ENABLE="1"
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin] # Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC" ARG TZ="Etc/UTC"
@ -205,7 +205,12 @@ ARG TARGETPLATFORM
RUN \ RUN \
# Use Ruby on Rails to create Mastodon assets # Use Ruby on Rails to create Mastodon assets
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \ ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=precompile_placeholder \
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=precompile_placeholder \
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=precompile_placeholder \
OTP_SECRET=precompile_placeholder \
SECRET_KEY_BASE=precompile_placeholder \
bundle exec rails assets:precompile; \
# Cleanup temporary files # Cleanup temporary files
rm -fr /opt/mastodon/tmp; rm -fr /opt/mastodon/tmp;

39
Gemfile
View file

@ -1,28 +1,28 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 3.0.0' ruby '>= 3.1.0'
gem 'puma', '~> 6.3'
gem 'rails', '~> 7.1.1'
gem 'propshaft' gem 'propshaft'
gem 'thor', '~> 1.2' gem 'puma', '~> 6.3'
gem 'rack', '~> 2.2.7' gem 'rack', '~> 2.2.7'
gem 'rails', '~> 7.1.1'
gem 'thor', '~> 1.2'
# For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182 # For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182
gem 'irb', '~> 1.8' gem 'irb', '~> 1.8'
gem 'dotenv'
gem 'haml-rails', '~>2.0' gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.5' gem 'pg', '~> 1.5'
gem 'pghero' gem 'pghero'
gem 'dotenv-rails', '~> 2.8'
gem 'aws-sdk-s3', '~> 1.123', require: false gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'blurhash', '~> 0.1'
gem 'fog-core', '<= 2.4.0' gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 1.0', require: false gem 'fog-openstack', '~> 1.0', require: false
gem 'kt-paperclip', '~> 7.2' gem 'kt-paperclip', '~> 7.2'
gem 'md-paperclip-azure', '~> 2.2', require: false gem 'md-paperclip-azure', '~> 2.2', require: false
gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8' gem 'addressable', '~> 2.8'
@ -39,11 +39,11 @@ end
gem 'net-ldap', '~> 0.18' gem 'net-ldap', '~> 0.18'
gem 'omniauth-cas', '~> 3.0.0.beta.1'
gem 'omniauth-saml', '~> 2.0'
gem 'omniauth_openid_connect', '~> 0.6.1'
gem 'omniauth', '~> 2.0' gem 'omniauth', '~> 2.0'
gem 'omniauth-cas', '~> 3.0.0.beta.1'
gem 'omniauth_openid_connect', '~> 0.6.1'
gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-saml', '~> 2.0'
gem 'color_diff', '~> 0.1' gem 'color_diff', '~> 0.1'
gem 'csv', '~> 3.2' gem 'csv', '~> 3.2'
@ -53,9 +53,8 @@ gem 'ed25519', '~> 1.3'
gem 'fast_blank', '~> 1.0' gem 'fast_blank', '~> 1.0'
gem 'fastimage' gem 'fastimage'
gem 'hiredis', '~> 0.6' gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.10'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 5.1' gem 'http', '~> 5.2.0'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.6.2' gem 'httplog', '~> 1.6.2'
gem 'i18n', '1.14.1' # TODO: Remove version when resolved: https://github.com/glebm/i18n-tasks/issues/552 / https://github.com/ruby-i18n/i18n/pull/688 gem 'i18n', '1.14.1' # TODO: Remove version when resolved: https://github.com/glebm/i18n-tasks/issues/552 / https://github.com/ruby-i18n/i18n/pull/688
@ -63,40 +62,40 @@ gem 'idn-ruby', require: 'idn'
gem 'inline_svg' gem 'inline_svg'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15' gem 'nokogiri', '~> 1.15'
gem 'nsa' gem 'nsa'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'
gem 'posix-spawn' gem 'premailer-rails'
gem 'public_suffix', '~> 5.0' gem 'public_suffix', '~> 5.0'
gem 'pundit', '~> 2.3' gem 'pundit', '~> 2.3'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.6' gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rack-cors', '~> 2.0', require: 'rack/cors'
gem 'rails-i18n', '~> 7.0' gem 'rails-i18n', '~> 7.0'
gem 'redcarpet', '~> 3.6' gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'redis-namespace', '~> 1.10'
gem 'rqrcode', '~> 2.2' gem 'rqrcode', '~> 2.2'
gem 'ruby-progressbar', '~> 1.13' gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 6.0' gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.7' gem 'scenic', '~> 1.7'
gem 'sidekiq', '~> 6.5' gem 'sidekiq', '~> 6.5'
gem 'sidekiq-bulk', '~> 0.2.0'
gem 'sidekiq-scheduler', '~> 5.0' gem 'sidekiq-scheduler', '~> 5.0'
gem 'sidekiq-unique-jobs', '~> 7.1' gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'sidekiq-bulk', '~> 0.2.0'
gem 'simple-navigation', '~> 4.4'
gem 'simple_form', '~> 5.2' gem 'simple_form', '~> 5.2'
gem 'stoplight', '~> 3.0.1' gem 'simple-navigation', '~> 4.4'
gem 'stoplight', '~> 4.1'
gem 'strong_migrations', '1.8.0' gem 'strong_migrations', '1.8.0'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023' gem 'tzinfo-data', '~> 1.2023'
gem 'webauthn', '~> 3.0'
gem 'webpacker', '~> 5.4' gem 'webpacker', '~> 5.4'
gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9'
gem 'webauthn', '~> 3.0'
gem 'json-ld' gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2' gem 'json-ld-preloaded', '~> 3.2'
@ -198,12 +197,14 @@ group :production do
gem 'lograge', '~> 0.12' gem 'lograge', '~> 0.12'
end end
gem 'cocoon', '~> 1.2'
gem 'concurrent-ruby', require: false gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1' gem 'xorcist', '~> 1.1'
gem 'cocoon', '~> 1.2'
gem 'net-http', '~> 0.4.0' gem 'net-http', '~> 0.4.0'
gem 'rubyzip', '~> 2.3' gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1' gem 'hcaptcha', '~> 7.1'
gem 'mail', '~> 2.8'

View file

@ -99,20 +99,20 @@ GEM
ast (2.4.2) ast (2.4.2)
attr_encrypted (4.0.0) attr_encrypted (4.0.0)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
attr_required (1.0.1) attr_required (1.0.2)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.873.0) aws-partitions (1.916.0)
aws-sdk-core (3.190.1) aws-sdk-core (3.192.1)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.75.0) aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.188.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.142.0) aws-sdk-s3 (1.147.0)
aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-core (~> 3, >= 3.192.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0) aws-sigv4 (1.8.0)
@ -132,7 +132,7 @@ GEM
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
rouge (>= 1.0.0) rouge (>= 1.0.0)
better_html (2.0.2) better_html (2.1.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
ast (~> 2.0) ast (~> 2.0)
@ -140,9 +140,9 @@ GEM
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.1.7) bigdecimal (3.1.7)
bindata (2.4.15) bindata (2.5.0)
binding_of_caller (1.0.0) binding_of_caller (1.0.1)
debug_inspector (>= 0.0.1) debug_inspector (>= 1.2.0)
blurhash (0.1.7) blurhash (0.1.7)
bootsnap (1.18.3) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
@ -167,7 +167,7 @@ GEM
xpath (~> 3.2) xpath (~> 3.2)
case_transform (0.2) case_transform (0.2)
activesupport activesupport
cbor (0.5.9.6) cbor (0.5.9.8)
charlock_holmes (0.7.7) charlock_holmes (0.7.7)
chewy (7.5.1) chewy (7.5.1)
activesupport (>= 5.2) activesupport (>= 5.2)
@ -182,23 +182,23 @@ GEM
cose (1.3.0) cose (1.3.0)
cbor (~> 0.5.9) cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0) openssl-signature_algorithm (~> 1.0)
crack (0.4.6) crack (1.0.0)
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.14.0) css_parser (1.17.1)
addressable addressable
csv (3.2.8) csv (3.3.0)
database_cleaner-active_record (2.1.0) database_cleaner-active_record (2.1.0)
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.3.4) date (3.3.4)
debug (1.9.1) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
debug_inspector (1.1.0) debug_inspector (1.2.0)
devise (4.9.3) devise (4.9.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@ -217,14 +217,10 @@ GEM
discard (1.3.0) discard (1.3.0)
activerecord (>= 4.2, < 8) activerecord (>= 4.2, < 8)
docile (1.4.0) docile (1.4.0)
domain_name (0.5.20190701) domain_name (0.6.20240107)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.6.9) doorkeeper (5.6.9)
railties (>= 5) railties (>= 5)
dotenv (2.8.1) dotenv (3.1.0)
dotenv-rails (2.8.1)
dotenv (= 2.8.1)
railties (>= 3.2)
drb (2.2.1) drb (2.2.1)
ed25519 (1.3.0) ed25519 (1.3.0)
elasticsearch (7.13.3) elasticsearch (7.13.3)
@ -242,11 +238,11 @@ GEM
mail (~> 2.7) mail (~> 2.7)
encryptor (3.0.0) encryptor (3.0.0)
erubi (1.12.0) erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.11)
tzinfo tzinfo
excon (0.109.0) excon (0.110.0)
fabrication (2.31.0) fabrication (2.31.0)
faker (3.2.3) faker (3.3.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)
@ -274,10 +270,10 @@ GEM
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fast_blank (1.0.1) fast_blank (1.0.1)
fastimage (2.3.0) fastimage (2.3.1)
ffi (1.15.5) ffi (1.16.3)
ffi-compiler (1.0.1) ffi-compiler (1.3.2)
ffi (>= 1.0.0) ffi (>= 1.15.5)
rake rake
fog-core (2.4.0) fog-core (2.4.0)
builder builder
@ -291,7 +287,7 @@ GEM
fog-core (~> 2.1) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
formatador (1.1.0) formatador (1.1.0)
fugit (1.8.1) fugit (1.10.1)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
fuubar (2.5.1) fuubar (2.5.1)
@ -318,15 +314,16 @@ GEM
hashie (5.0.0) hashie (5.0.0)
hcaptcha (7.1.0) hcaptcha (7.1.0)
json json
highline (2.1.0) highline (3.0.1)
hiredis (0.6.3) hiredis (0.6.3)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
http (5.1.1) http (5.2.0)
addressable (~> 2.8) addressable (~> 2.8)
base64 (~> 0.1)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 2.2) http-form_data (~> 2.2)
llhttp-ffi (~> 0.4.0) llhttp-ffi (~> 0.5.0)
http-cookie (1.0.5) http-cookie (1.0.5)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (2.3.0) http-form_data (2.3.0)
@ -357,7 +354,7 @@ GEM
rdoc rdoc
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.1) json (2.7.2)
json-canonicalization (1.0.0) json-canonicalization (1.0.0)
json-jwt (1.15.3.1) json-jwt (1.15.3.1)
activesupport (>= 4.2) activesupport (>= 4.2)
@ -374,7 +371,7 @@ GEM
json-ld-preloaded (3.3.0) json-ld-preloaded (3.3.0)
json-ld (~> 3.3) json-ld (~> 3.3)
rdf (~> 3.3) rdf (~> 3.3)
json-schema (4.2.0) json-schema (4.3.0)
addressable (>= 2.8) addressable (>= 2.8)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.7.1) jwt (2.7.1)
@ -399,15 +396,15 @@ GEM
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (2.5.2) launchy (2.5.2)
addressable (~> 2.8) addressable (~> 2.8)
letter_opener (1.8.1) letter_opener (1.10.0)
launchy (>= 2.2, < 3) launchy (>= 2.2, < 4)
letter_opener_web (2.0.0) letter_opener_web (2.0.0)
actionmailer (>= 5.2) actionmailer (>= 5.2)
letter_opener (~> 1.7) letter_opener (~> 1.7)
railties (>= 5.2) railties (>= 5.2)
rexml rexml
link_header (0.0.8) link_header (0.0.8)
llhttp-ffi (0.4.0) llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
lograge (0.14.0) lograge (0.14.0)
@ -423,7 +420,7 @@ GEM
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
marcel (1.0.2) marcel (1.0.4)
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
matrix (0.4.2) matrix (0.4.2)
@ -434,13 +431,13 @@ GEM
memory_profiler (1.0.1) memory_profiler (1.0.1)
mime-types (3.5.2) mime-types (3.5.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.1205) mime-types-data (3.2024.0305)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.5) mini_portile2 (2.8.6)
minitest (5.22.3) minitest (5.22.3)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.4.0)
mutex_m (0.2.0) mutex_m (0.2.0)
net-http (0.4.1) net-http (0.4.1)
uri uri
@ -454,10 +451,10 @@ GEM
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
timeout timeout
net-smtp (0.4.0.1) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.5.9) nio4r (2.7.1)
nokogiri (1.16.3) nokogiri (1.16.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nsa (0.3.0) nsa (0.3.0)
@ -510,8 +507,7 @@ GEM
pg (1.5.6) pg (1.5.6)
pghero (3.4.1) pghero (3.4.1)
activerecord (>= 6) activerecord (>= 6)
posix-spawn (0.3.15) premailer (1.23.0)
premailer (1.21.0)
addressable addressable
css_parser (>= 1.12.0) css_parser (>= 1.12.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
@ -527,7 +523,7 @@ GEM
railties (>= 7.0.0) railties (>= 7.0.0)
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (5.0.4) public_suffix (5.0.5)
puma (6.4.2) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.1) pundit (2.3.1)
@ -548,7 +544,7 @@ GEM
rack-protection (3.2.0) rack-protection (3.2.0)
base64 (>= 0.1.0) base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4) rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6) rack-proxy (0.7.7)
rack rack
rack-session (1.0.2) rack-session (1.0.2)
rack (< 3) rack (< 3)
@ -594,7 +590,7 @@ GEM
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.1.0) rake (13.2.1)
rdf (3.3.1) rdf (3.3.1)
bcp47_spec (~> 0.2) bcp47_spec (~> 0.2)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
@ -609,16 +605,16 @@ GEM
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.9.0) regexp_parser (2.9.0)
reline (0.4.3) reline (0.5.2)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.5.1) request_store (1.6.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.2.6) rexml (3.2.6)
rotp (6.3.0) rotp (6.3.0)
rouge (4.1.2) rouge (4.2.1)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
@ -642,13 +638,13 @@ GEM
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-sidekiq (4.1.0) rspec-sidekiq (4.2.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
rspec-expectations (~> 3.0) rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8) sidekiq (>= 5, < 8)
rspec-support (3.13.1) rspec-support (3.13.1)
rubocop (1.62.1) rubocop (1.63.3)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
@ -665,21 +661,24 @@ GEM
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.25.1) rubocop-factory_bot (2.25.1)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-performance (1.20.2) rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.30.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.24.0) rubocop-rails (2.24.1)
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-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.27.1) rubocop-rspec (2.29.1)
rubocop (~> 1.40) rubocop (~> 1.40)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
rubocop-rspec_rails (~> 2.28)
rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40)
ruby-prof (1.7.0) ruby-prof (1.7.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.15.0) ruby-saml (1.16.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
rexml rexml
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
@ -691,10 +690,10 @@ GEM
sanitize (6.1.0) sanitize (6.1.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
scenic (1.7.0) scenic (1.8.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
selenium-webdriver (4.18.1) selenium-webdriver (4.19.0)
base64 (~> 0.2) base64 (~> 0.2)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
@ -731,7 +730,7 @@ GEM
smart_properties (1.17.0) smart_properties (1.17.0)
stackprof (0.2.26) stackprof (0.2.26)
statsd-ruby (1.5.0) statsd-ruby (1.5.0)
stoplight (3.0.2) stoplight (4.1.0)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.1.0) stringio (3.1.0)
strong_migrations (1.8.0) strong_migrations (1.8.0)
@ -746,7 +745,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1) terrapin (1.0.1)
climate_control climate_control
test-prof (1.3.2) test-prof (1.3.3)
thor (1.3.1) thor (1.3.1)
tilt (2.3.0) tilt (2.3.0)
timeout (0.4.1) timeout (0.4.1)
@ -763,7 +762,7 @@ GEM
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
tty-screen (~> 0.8) tty-screen (~> 0.8)
wisper (~> 2.0) wisper (~> 2.0)
tty-screen (0.8.1) tty-screen (0.8.2)
twitter-text (3.1.0) twitter-text (3.1.0)
idn-ruby idn-ruby
unf (~> 0.1.0) unf (~> 0.1.0)
@ -773,9 +772,9 @@ GEM
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.9.1)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
uri (0.12.2) uri (0.13.0)
validate_email (0.1.6) validate_email (0.1.6)
activemodel (>= 3.0) activemodel (>= 3.0)
mail (>= 2.2.5) mail (>= 2.2.5)
@ -796,7 +795,7 @@ GEM
webfinger (1.2.0) webfinger (1.2.0)
activesupport activesupport
httpclient (>= 2.4) httpclient (>= 2.4)
webmock (3.22.0) webmock (3.23.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
@ -847,7 +846,7 @@ DEPENDENCIES
devise_pam_authenticatable2 (~> 9.2) devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.2) discard (~> 1.2)
doorkeeper (~> 5.6) doorkeeper (~> 5.6)
dotenv-rails (~> 2.8) dotenv
ed25519 (~> 1.3) ed25519 (~> 1.3)
email_spec email_spec
fabrication (~> 2.30) fabrication (~> 2.30)
@ -862,7 +861,7 @@ DEPENDENCIES
hcaptcha (~> 7.1) hcaptcha (~> 7.1)
hiredis (~> 0.6) hiredis (~> 0.6)
htmlentities (~> 4.3) htmlentities (~> 4.3)
http (~> 5.1) http (~> 5.2.0)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
httplog (~> 1.6.2) httplog (~> 1.6.2)
i18n (= 1.14.1) i18n (= 1.14.1)
@ -879,6 +878,7 @@ DEPENDENCIES
letter_opener_web (~> 2.0) letter_opener_web (~> 2.0)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.12) lograge (~> 0.12)
mail (~> 2.8)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
md-paperclip-azure (~> 2.2) md-paperclip-azure (~> 2.2)
memory_profiler memory_profiler
@ -897,7 +897,6 @@ DEPENDENCIES
parslet parslet
pg (~> 1.5) pg (~> 1.5)
pghero pghero
posix-spawn
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
propshaft propshaft
@ -939,7 +938,7 @@ DEPENDENCIES
simplecov (~> 0.22) simplecov (~> 0.22)
simplecov-lcov (~> 0.8) simplecov-lcov (~> 0.8)
stackprof stackprof
stoplight (~> 3.0.1) stoplight (~> 4.1)
strong_migrations (= 1.8.0) strong_migrations (= 1.8.0)
test-prof test-prof
thor (~> 1.2) thor (~> 1.2)
@ -953,7 +952,7 @@ DEPENDENCIES
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION RUBY VERSION
ruby 3.2.2p53 ruby 3.2.3p157
BUNDLED WITH BUNDLED WITH
2.5.4 2.5.9

View file

@ -69,7 +69,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
- **PostgreSQL** 12+ - **PostgreSQL** 12+
- **Redis** 4+ - **Redis** 4+
- **Ruby** 3.0+ - **Ruby** 3.1+
- **Node.js** 16+ - **Node.js** 16+
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.

1
Vagrantfile vendored
View file

@ -173,6 +173,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080 # Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080
config.vm.network :forwarded_port, guest: 3000, host: 3000 config.vm.network :forwarded_port, guest: 3000, host: 3000
config.vm.network :forwarded_port, guest: 3035, host: 3035
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: 9200, host: 9200

View file

@ -46,7 +46,7 @@ class AccountsController < ApplicationController
end end
def default_statuses def default_statuses
@account.statuses.where(visibility: [:public, :unlisted]) @account.statuses.distributable_visibility
end end
def only_media_scope def only_media_scope

View file

@ -31,7 +31,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
def set_replies def set_replies
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses @replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) @replies = @replies.distributable_visibility.where(in_reply_to_id: @status.id)
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
end end

View file

@ -9,6 +9,7 @@ class Api::BaseController < ApplicationController
include Api::CachingConcern include Api::CachingConcern
include Api::ContentSecurityPolicy include Api::ContentSecurityPolicy
include Api::ErrorHandling include Api::ErrorHandling
include Api::Pagination
skip_before_action :require_functional!, unless: :limited_federation_mode? skip_before_action :require_functional!, unless: :limited_federation_mode?
@ -29,21 +30,6 @@ class Api::BaseController < ApplicationController
protected protected
def pagination_max_id
pagination_collection.last.id
end
def pagination_since_id
pagination_collection.first.id
end
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path
links << [prev_path, [%w(rel prev)]] if prev_path
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
end
def limit_param(default_limit) def limit_param(default_limit)
return default_limit unless params[:limit] return default_limit unless params[:limit]
@ -72,10 +58,6 @@ class Api::BaseController < ApplicationController
render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.unavailable? render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.unavailable?
end end
def require_valid_pagination_options!
render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid?
end
def require_user! def require_user!
if !current_user if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422 render json: { error: 'This method requires an authenticated user' }, status: 422
@ -104,14 +86,6 @@ class Api::BaseController < ApplicationController
private private
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_options_invalid?
params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?)
end
def respond_with_error(code) def respond_with_error(code)
render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code
end end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Accounts::CredentialsController < Api::BaseController class Api::V1::Accounts::CredentialsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:update] before_action -> { doorkeeper_authorize! :read, :'read:accounts', :'read:me' }, except: [:update]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update] before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update]
before_action :require_user! before_action :require_user!

View file

@ -12,10 +12,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
private private
def set_recently_used_tags def set_recently_used_tags
@recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10) @recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
end
def featured_tag_ids
current_account.featured_tags.pluck(:tag_id)
end end
end end

View file

@ -23,7 +23,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base
end end
def paginated_statuses def paginated_statuses
Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted]).paginate_by_max_id( Status.where(reblog_of_id: @status.id).distributable_visibility.paginate_by_max_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),
params[:max_id], params[:max_id],
params[:since_id] params[:since_id]

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Api::Pagination
extend ActiveSupport::Concern
protected
def pagination_max_id
pagination_collection.last.id
end
def pagination_since_id
pagination_collection.first.id
end
def set_pagination_headers(next_path = nil, prev_path = nil)
links = []
links << [next_path, [%w(rel next)]] if next_path
links << [prev_path, [%w(rel prev)]] if prev_path
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
end
def require_valid_pagination_options!
render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid?
end
private
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_options_invalid?
params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?)
end
end

View file

@ -46,27 +46,19 @@ module CacheConcern
end end
end end
# TODO: Rename this method, as it does not perform any caching anymore.
def cache_collection(raw, klass) def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes) return raw unless klass.respond_to?(:preload_cacheable_associations)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) records = raw.to_a
return [] if raw.empty?
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) klass.preload_cacheable_associations(records)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys records
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
Rails.cache.write_multi(uncached.values.to_h { |i| [i, i] })
end
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
end end
# TODO: Rename this method, as it does not perform any caching anymore.
def cache_collection_paginated_by_id(raw, klass, limit, options) def cache_collection_paginated_by_id(raw, klass, limit, options)
cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass cache_collection raw.to_a_paginated_by_id(limit, options), klass
end end
end end

View file

@ -66,7 +66,7 @@ module SignatureVerification
compare_signed_string = build_signed_string(include_query_string: false) compare_signed_string = build_signed_string(include_query_string: false)
return actor unless verify_signature(actor, signature, compare_signed_string).nil? return actor unless verify_signature(actor, signature, compare_signed_string).nil?
actor = stoplight_wrap_request { actor_refresh_key!(actor) } actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
@ -226,10 +226,10 @@ module SignatureVerification
end end
if key_id.start_with?('acct:') if key_id.start_with?('acct:')
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
elsif !ActivityPub::TagManager.instance.local_uri?(key_id) elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
account = ActivityPub::TagManager.instance.uri_to_actor(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) } account ||= stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
account account
end end
rescue Mastodon::PrivateNetworkAddressError => e rescue Mastodon::PrivateNetworkAddressError => e
@ -238,12 +238,11 @@ module SignatureVerification
raise SignatureVerificationError, e.message raise SignatureVerificationError, e.message
end end
def stoplight_wrap_request(&block) def stoplight_wrapper
Stoplight("source:#{request.remote_ip}", &block) Stoplight("source:#{request.remote_ip}")
.with_threshold(1) .with_threshold(1)
.with_cool_off_time(5.minutes.seconds) .with_cool_off_time(5.minutes.seconds)
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
.run
end end
def actor_refresh_key!(actor) def actor_refresh_key!(actor)

View file

@ -38,7 +38,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
end end
def set_recently_used_tags def set_recently_used_tags
@recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10) @recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
end end
def featured_tag_params def featured_tag_params

View file

@ -31,7 +31,7 @@ class Settings::ImportsController < Settings::BaseController
def show; end def show; end
def failures def failures
@bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id]) @bulk_import = current_account.bulk_imports.state_finished.find(params[:id])
respond_to do |format| respond_to do |format|
format.csv do format.csv do
@ -92,7 +92,7 @@ class Settings::ImportsController < Settings::BaseController
end end
def set_bulk_import def set_bulk_import
@bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id]) @bulk_import = current_account.bulk_imports.state_unconfirmed.find(params[:id])
end end
def set_recent_imports def set_recent_imports

View file

@ -113,6 +113,14 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end end
def material_symbol(icon, attributes = {})
inline_svg_tag(
"400-24px/#{icon}.svg",
class: %w(icon).concat(attributes[:class].to_s.split),
role: :img
)
end
def check_icon def check_icon
inline_svg_tag 'check.svg' inline_svg_tag 'check.svg'
end end

View file

@ -19,6 +19,6 @@ module BrandingHelper
end end
def render_logo def render_logo
image_pack_tag('logo.svg', alt: 'Mastodon', class: 'logo logo--icon') image_tag(frontend_asset_path('images/logo.svg'), alt: 'Mastodon', class: 'logo logo--icon')
end end
end end

View file

@ -48,13 +48,11 @@ module ContextHelper
end end
def serialized_context(named_contexts_map, context_extensions_map) def serialized_context(named_contexts_map, context_extensions_map)
context_array = []
named_contexts = named_contexts_map.keys named_contexts = named_contexts_map.keys
context_extensions = context_extensions_map.keys context_extensions = context_extensions_map.keys
named_contexts.each do |key| context_array = named_contexts.map do |key|
context_array << NAMED_CONTEXT_MAP[key] NAMED_CONTEXT_MAP[key]
end end
extensions = context_extensions.each_with_object({}) do |key, h| extensions = context_extensions.each_with_object({}) do |key, h|

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
module ThemeHelper
def theme_style_tags(theme)
if theme == 'system'
stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') +
stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
else
stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous'
end
end
def theme_color_tags(theme)
if theme == 'system'
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') +
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
else
tag.meta name: 'theme-color', content: theme_color_for(theme)
end
end
private
def theme_color_for(theme)
theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark]
end
end

View file

@ -363,6 +363,6 @@ ready(() => {
document.querySelectorAll('[data-admin-component]').forEach((element) => { document.querySelectorAll('[data-admin-component]').forEach((element) => {
void mountReactComponent(element); void mountReactComponent(element);
}); });
}).catch((reason) => { }).catch((reason: unknown) => {
throw reason; throw reason;
}); });

View file

@ -69,7 +69,7 @@ window.addEventListener('message', (e) => {
}, },
'*', '*',
); );
}).catch((e) => { }).catch((e: unknown) => {
console.error('Error in setHeightMessage postMessage', e); console.error('Error in setHeightMessage postMessage', e);
}); });
}); });
@ -206,7 +206,7 @@ function loaded() {
return true; return true;
}) })
.catch((error) => { .catch((error: unknown) => {
console.error(error); console.error(error);
}); });
} }
@ -448,7 +448,7 @@ Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
}); });
function main() { function main() {
ready(loaded).catch((error) => { ready(loaded).catch((error: unknown) => {
console.error(error); console.error(error);
}); });
} }
@ -457,6 +457,6 @@ loadPolyfills()
.then(loadLocale) .then(loadLocale)
.then(main) .then(main)
.then(loadKeyboardExtensions) .then(loadKeyboardExtensions)
.catch((error) => { .catch((error: unknown) => {
console.error(error); console.error(error);
}); });

View file

@ -1,32 +0,0 @@
import { openModal } from './modal';
export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
export function initBoostModal(props) {
return (dispatch, getState) => {
const default_privacy = getState().getIn(['compose', 'default_privacy']);
const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
dispatch({
type: BOOSTS_INIT_MODAL,
privacy,
});
dispatch(openModal({
modalType: 'BOOST',
modalProps: props,
}));
};
}
export function changeBoostPrivacy(privacy) {
return dispatch => {
dispatch({
type: BOOSTS_CHANGE_PRIVACY,
privacy,
});
};
}

View file

@ -1,152 +0,0 @@
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import api from '../api';
import { compareId } from '../compare_id';
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL';
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if (window.fetch && 'keepalive' in new Request('')) {
fetch('/api/v1/markers', {
keepalive: true,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
} else if (navigator && navigator.sendBeacon) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) {
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon('/api/v1/markers', formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}
};
const _buildParams = (state) => {
const params = {};
const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token'], '');
const params = _buildParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return;
}
api(getState).post('/api/v1/markers', params).then(() => {
dispatch(submitMarkersSuccess(params));
}).catch(() => {});
}, 300000, { leading: true, trailing: true });
export function submitMarkersSuccess({ home, notifications }) {
return {
type: MARKERS_SUBMIT_SUCCESS,
home: (home || {}).last_read_id,
notifications: (notifications || {}).last_read_id,
};
}
export function submitMarkers(params = {}) {
const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
if (params.immediate === true) {
debouncedSubmitMarkers.flush();
}
return result;
}
export const fetchMarkers = () => (dispatch, getState) => {
const params = { timeline: ['notifications'] };
dispatch(fetchMarkersRequest());
api(getState).get('/api/v1/markers', { params }).then(response => {
dispatch(fetchMarkersSuccess(response.data));
}).catch(error => {
dispatch(fetchMarkersFail(error));
});
};
export function fetchMarkersRequest() {
return {
type: MARKERS_FETCH_REQUEST,
skipLoading: true,
};
}
export function fetchMarkersSuccess(markers) {
return {
type: MARKERS_FETCH_SUCCESS,
markers,
skipLoading: true,
};
}
export function fetchMarkersFail(error) {
return {
type: MARKERS_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
};
}

View file

@ -0,0 +1,165 @@
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import type { MarkerJSON } from 'mastodon/api_types/markers';
import type { RootState } from 'mastodon/store';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
import api, { authorizationTokenFromState } from '../api';
import { compareId } from '../compare_id';
export const synchronouslySubmitMarkers = createAppAsyncThunk(
'markers/submit',
async (_args, { getState }) => {
const accessToken = authorizationTokenFromState(getState);
const params = buildPostMarkersParams(getState());
if (Object.keys(params).length === 0 || !accessToken) {
return;
}
// The Fetch API allows us to perform requests that will be carried out
// after the page closes. But that only works if the `keepalive` attribute
// is supported.
if ('fetch' in window && 'keepalive' in new Request('')) {
await fetch('/api/v1/markers', {
keepalive: true,
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(params),
});
return;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if ('navigator' && 'sendBeacon' in navigator) {
// Failing that, we can use sendBeacon, but we have to encode the data as
// FormData for DoorKeeper to recognize the token.
const formData = new FormData();
formData.append('bearer_token', accessToken);
for (const [id, value] of Object.entries(params)) {
if (value.last_read_id)
formData.append(`${id}[last_read_id]`, value.last_read_id);
}
if (navigator.sendBeacon('/api/v1/markers', formData)) {
return;
}
}
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
// request.
try {
const client = new XMLHttpRequest();
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}
},
);
interface MarkerParam {
last_read_id?: string;
}
function getLastHomeId(state: RootState): string | undefined {
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
return (
state
// @ts-expect-error state.timelines is not yet typed
.getIn(['timelines', 'home', 'items'], ImmutableList())
// @ts-expect-error state.timelines is not yet typed
.find((item) => item !== null)
);
}
function getLastNotificationId(state: RootState): string | undefined {
// @ts-expect-error state.notifications is not yet typed
return state.getIn(['notifications', 'lastReadId']);
}
const buildPostMarkersParams = (state: RootState) => {
const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
const lastHomeId = getLastHomeId(state);
const lastNotificationId = getLastNotificationId(state);
if (lastHomeId && compareId(lastHomeId, state.markers.home) > 0) {
params.home = {
last_read_id: lastHomeId,
};
}
if (
lastNotificationId &&
compareId(lastNotificationId, state.markers.notifications) > 0
) {
params.notifications = {
last_read_id: lastNotificationId,
};
}
return params;
};
export const submitMarkersAction = createAppAsyncThunk<{
home: string | undefined;
notifications: string | undefined;
}>('markers/submitAction', async (_args, { getState }) => {
const accessToken = authorizationTokenFromState(getState);
const params = buildPostMarkersParams(getState());
if (Object.keys(params).length === 0 || accessToken === '') {
return { home: undefined, notifications: undefined };
}
await api(getState).post<MarkerJSON>('/api/v1/markers', params);
return {
home: params.home?.last_read_id,
notifications: params.notifications?.last_read_id,
};
});
const debouncedSubmitMarkers = debounce(
(dispatch) => {
dispatch(submitMarkersAction());
},
300000,
{
leading: true,
trailing: true,
},
);
export const submitMarkers = createAppAsyncThunk(
'markers/submit',
(params: { immediate?: boolean }, { dispatch }) => {
debouncedSubmitMarkers(dispatch);
if (params.immediate) {
debouncedSubmitMarkers.flush();
}
},
);
export const fetchMarkers = createAppAsyncThunk(
'markers/fetch',
async (_args, { getState }) => {
const response = await api(getState).get<Record<string, MarkerJSON>>(
`/api/v1/markers`,
{ params: { timeline: ['notifications'] } },
);
return { markers: response.data };
},
);

View file

@ -1,46 +0,0 @@
// @ts-check
export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY';
export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
/**
* @typedef MediaProps
* @property {string} src
* @property {boolean} muted
* @property {number} volume
* @property {number} currentTime
* @property {string} poster
* @property {string} backgroundColor
* @property {string} foregroundColor
* @property {string} accentColor
*/
/**
* @param {string} statusId
* @param {string} accountId
* @param {string} playerType
* @param {MediaProps} props
* @returns {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
// @ts-expect-error
return (dispatch, getState) => {
// Do not open a player for a toot that does not exist
if (getState().hasIn(['statuses', statusId])) {
dispatch({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
}
};
};
/*
* @return {object}
*/
export const removePictureInPicture = () => ({
type: PICTURE_IN_PICTURE_REMOVE,
});

View file

@ -0,0 +1,31 @@
import { createAction } from '@reduxjs/toolkit';
import type { PIPMediaProps } from 'mastodon/reducers/picture_in_picture';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
interface DeployParams {
statusId: string;
accountId: string;
playerType: 'audio' | 'video';
props: PIPMediaProps;
}
export const removePictureInPicture = createAction('pip/remove');
export const deployPictureInPictureAction =
createAction<DeployParams>('pip/deploy');
export const deployPictureInPicture = createAppAsyncThunk(
'pip/deploy',
(args: DeployParams, { dispatch, getState }) => {
const { statusId } = args;
// Do not open a player for a toot that does not exist
// @ts-expect-error state.statuses is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
if (getState().hasIn(['statuses', statusId])) {
dispatch(deployPictureInPictureAction(args));
}
},
);

View file

@ -29,9 +29,14 @@ const setCSRFHeader = () => {
void ready(setCSRFHeader); void ready(setCSRFHeader);
export const authorizationTokenFromState = (getState?: GetState) => {
return (
getState && (getState().meta.get('access_token', '') as string | false)
);
};
const authorizationHeaderFromState = (getState?: GetState) => { const authorizationHeaderFromState = (getState?: GetState) => {
const accessToken = const accessToken = authorizationTokenFromState(getState);
getState && (getState().meta.get('access_token', '') as string);
if (!accessToken) { if (!accessToken) {
return {}; return {};

View file

@ -0,0 +1,7 @@
// See app/serializers/rest/account_serializer.rb
export interface MarkerJSON {
last_read_id: string;
version: string;
updated_at: string;
}

View file

@ -0,0 +1,22 @@
// See app/serializers/rest/media_attachment_serializer.rb
export type MediaAttachmentType =
| 'image'
| 'gifv'
| 'video'
| 'unknown'
| 'audio';
export interface ApiMediaAttachmentJSON {
id: string;
type: MediaAttachmentType;
url: string;
preview_url: string;
remoteUrl: string;
preview_remote_url: string;
text_url: string;
// TODO: how to define this?
meta: unknown;
description?: string;
blurhash: string;
}

View file

@ -0,0 +1,23 @@
import type { ApiCustomEmojiJSON } from './custom_emoji';
// See app/serializers/rest/poll_serializer.rb
export interface ApiPollOptionJSON {
title: string;
votes_count: number;
}
export interface ApiPollJSON {
id: string;
expires_at: string;
expired: boolean;
multiple: boolean;
votes_count: number;
voters_count: number;
options: ApiPollOptionJSON[];
emojis: ApiCustomEmojiJSON[];
voted: boolean;
own_votes: number[];
}

View file

@ -0,0 +1,91 @@
// See app/serializers/rest/status_serializer.rb
import type { ApiAccountJSON } from './accounts';
import type { ApiCustomEmojiJSON } from './custom_emoji';
import type { ApiMediaAttachmentJSON } from './media_attachments';
import type { ApiPollJSON } from './polls';
// See app/modals/status.rb
export type StatusVisibility =
| 'public'
| 'unlisted'
| 'private'
// | 'limited' // This is never exposed to the API (they become `private`)
| 'direct';
export interface ApiStatusApplicationJSON {
name: string;
website: string;
}
export interface ApiTagJSON {
name: string;
url: string;
}
export interface ApiMentionJSON {
id: string;
username: string;
url: string;
acct: string;
}
export interface ApiPreviewCardJSON {
url: string;
title: string;
description: string;
language: string;
type: string;
author_name: string;
author_url: string;
provider_name: string;
provider_url: string;
html: string;
width: number;
height: number;
image: string;
image_description: string;
embed_url: string;
blurhash: string;
published_at: string;
}
export interface ApiStatusJSON {
id: string;
created_at: string;
in_reply_to_id?: string;
in_reply_to_account_id?: string;
sensitive: boolean;
spoiler_text?: string;
visibility: StatusVisibility;
language: string;
uri: string;
url: string;
replies_count: number;
reblogs_count: number;
favorites_count: number;
edited_at?: string;
favorited?: boolean;
reblogged?: boolean;
muted?: boolean;
bookmarked?: boolean;
pinned?: boolean;
// filtered: FilterResult[]
filtered: unknown; // TODO
content?: string;
text?: string;
reblog?: ApiStatusJSON;
application?: ApiStatusApplicationJSON;
account: ApiAccountJSON;
media_attachments: ApiMediaAttachmentJSON[];
mentions: ApiMentionJSON[];
tags: ApiTagJSON[];
emojis: ApiCustomEmojiJSON[];
card?: ApiPreviewCardJSON;
poll?: ApiPollJSON;
}

View file

@ -2,7 +2,7 @@ import Rails from '@rails/ujs';
import 'font-awesome/css/font-awesome.css'; import 'font-awesome/css/font-awesome.css';
export function start() { export function start() {
require.context('../images/', true); require.context('../images/', true, /\.(jpg|png|svg)$/);
try { try {
Rails.start(); Rails.start();

View file

@ -1,17 +1,19 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { EmptyAccount } from 'mastodon/components/empty_account'; import { EmptyAccount } from 'mastodon/components/empty_account';
import { ShortNumber } from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { VerifiedBadge } from 'mastodon/components/verified_badge';
import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { me } from '../initial_state'; import { me } from '../initial_state';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
@ -30,151 +32,151 @@ const messages = defineMessages({
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' }, unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
mute: { id: 'account.mute_short', defaultMessage: 'Mute' }, mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' }, block: { id: 'account.block_short', defaultMessage: 'Block' },
more: { id: 'status.more', defaultMessage: 'More' },
}); });
class Account extends ImmutablePureComponent { const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, minimal, defaultAction, withBio }) => {
const intl = useIntl();
static propTypes = { const handleFollow = useCallback(() => {
size: PropTypes.number, onFollow(account);
account: ImmutablePropTypes.record, }, [onFollow, account]);
onFollow: PropTypes.func,
onBlock: PropTypes.func,
onMute: PropTypes.func,
onMuteNotifications: PropTypes.func,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
minimal: PropTypes.bool,
defaultAction: PropTypes.string,
withBio: PropTypes.bool,
};
static defaultProps = { const handleBlock = useCallback(() => {
size: 46, onBlock(account);
}; }, [onBlock, account]);
handleFollow = () => { const handleMute = useCallback(() => {
this.props.onFollow(this.props.account); onMute(account);
}; }, [onMute, account]);
handleBlock = () => { const handleMuteNotifications = useCallback(() => {
this.props.onBlock(this.props.account); onMuteNotifications(account, true);
}; }, [onMuteNotifications, account]);
handleMute = () => { const handleUnmuteNotifications = useCallback(() => {
this.props.onMute(this.props.account); onMuteNotifications(account, false);
}; }, [onMuteNotifications, account]);
handleMuteNotifications = () => { if (!account) {
this.props.onMuteNotifications(this.props.account, true); return <EmptyAccount size={size} minimal={minimal} />;
}; }
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
};
render () {
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
if (!account) {
return <EmptyAccount size={size} minimal={minimal} />;
}
if (hidden) {
return (
<>
{account.get('display_name')}
{account.get('username')}
</>
);
}
let buttons;
if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
} else if (blocking) {
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
} else if (muting) {
let hidingNotificationsButton;
if (account.getIn(['relationship', 'muting_notifications'])) {
hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
}
buttons = (
<>
<Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
{hidingNotificationsButton}
</>
);
} else if (defaultAction === 'mute') {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
} else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
} else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
}
}
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
}
if (hidden) {
return ( return (
<div className={classNames('account', { 'account--minimal': minimal })}> <>
<div className='account__wrapper'> {account.get('display_name')}
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}> {account.get('username')}
<div className='account__avatar-wrapper'> </>
<Avatar account={account} size={size} />
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
</div>
)}
</div>
</Link>
{!minimal && (
<div className='account__relationship'>
{buttons}
</div>
)}
</div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div>
); );
} }
} let buttons;
export default injectIntl(Account); if (account.get('id') !== me && account.get('relationship', null) !== null) {
const following = account.getIn(['relationship', 'following']);
const requested = account.getIn(['relationship', 'requested']);
const blocking = account.getIn(['relationship', 'blocking']);
const muting = account.getIn(['relationship', 'muting']);
if (requested) {
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={handleFollow} />;
} else if (blocking) {
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={handleBlock} />;
} else if (muting) {
let menu;
if (account.getIn(['relationship', 'muting_notifications'])) {
menu = [{ text: intl.formatMessage(messages.unmute_notifications), action: handleUnmuteNotifications }];
} else {
menu = [{ text: intl.formatMessage(messages.mute_notifications), action: handleMuteNotifications }];
}
buttons = (
<>
<DropdownMenuContainer
items={menu}
icon='ellipsis-h'
iconComponent={MoreHorizIcon}
direction='right'
title={intl.formatMessage(messages.more)}
/>
<Button text={intl.formatMessage(messages.unmute)} onClick={handleMute} />
</>
);
} else if (defaultAction === 'mute') {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={handleMute} />;
} else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
} else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={handleFollow} />;
}
}
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
}
return (
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'>
<Avatar account={account} size={size} />
</div>
<div className='account__contents'>
<DisplayName account={account} />
{!minimal && (
<div className='account__details'>
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
</div>
)}
</div>
</Link>
{!minimal && (
<div className='account__relationship'>
{buttons}
</div>
)}
</div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div>
);
};
Account.propTypes = {
size: PropTypes.number,
account: ImmutablePropTypes.record,
onFollow: PropTypes.func,
onBlock: PropTypes.func,
onMute: PropTypes.func,
onMuteNotifications: PropTypes.func,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
minimal: PropTypes.bool,
defaultAction: PropTypes.string,
withBio: PropTypes.bool,
};
export default Account;

View file

@ -1,26 +1,26 @@
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
interface BaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { interface BaseProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
block?: boolean; block?: boolean;
secondary?: boolean; secondary?: boolean;
text?: JSX.Element;
} }
interface PropsWithChildren extends BaseProps { interface PropsChildren extends PropsWithChildren<BaseProps> {
text?: never; text?: undefined;
} }
interface PropsWithText extends BaseProps { interface PropsWithText extends BaseProps {
text: JSX.Element; text: JSX.Element | string;
children: never; children?: undefined;
} }
type Props = PropsWithText | PropsWithChildren; type Props = PropsWithText | PropsChildren;
export const Button: React.FC<Props> = ({ export const Button: React.FC<Props> = ({
text,
type = 'button', type = 'button',
onClick, onClick,
disabled, disabled,
@ -28,6 +28,7 @@ export const Button: React.FC<Props> = ({
secondary, secondary,
className, className,
title, title,
text,
children, children,
...props ...props
}) => { }) => {

View file

@ -24,7 +24,7 @@ export type StatusLike = Record<{
function normalizeHashtag(hashtag: string) { function normalizeHashtag(hashtag: string) {
return ( return (
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag !!hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
).normalize('NFKC'); ).normalize('NFKC');
} }

View file

@ -191,7 +191,7 @@ const timeRemainingString = (
interface Props { interface Props {
intl: IntlShape; intl: IntlShape;
timestamp: string; timestamp: string;
year: number; year?: number;
futureDate?: boolean; futureDate?: boolean;
short?: boolean; short?: boolean;
} }
@ -203,11 +203,6 @@ class RelativeTimestamp extends Component<Props, States> {
now: Date.now(), now: Date.now(),
}; };
static defaultProps = {
year: new Date().getFullYear(),
short: true,
};
_timer: number | undefined; _timer: number | undefined;
shouldComponentUpdate(nextProps: Props, nextState: States) { shouldComponentUpdate(nextProps: Props, nextState: States) {
@ -257,7 +252,13 @@ class RelativeTimestamp extends Component<Props, States> {
} }
render() { render() {
const { timestamp, intl, year, futureDate, short } = this.props; const {
timestamp,
intl,
futureDate,
year = new Date().getFullYear(),
short = true,
} = this.props;
const timeGiven = timestamp.includes('T'); const timeGiven = timestamp.includes('T');
const date = new Date(timestamp); const date = new Date(timestamp);

View file

@ -81,7 +81,7 @@ class StatusActionBar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map, relationship: ImmutablePropTypes.record,
onReply: PropTypes.func, onReply: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
onReblog: PropTypes.func, onReblog: PropTypes.func,

View file

@ -4,11 +4,10 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import type { StatusVisibility } from 'mastodon/models/status';
import { Icon } from './icon'; import { Icon } from './icon';
type Visibility = 'public' | 'unlisted' | 'private' | 'direct';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
unlisted_short: { unlisted_short: {
@ -25,7 +24,7 @@ const messages = defineMessages({
}, },
}); });
export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({ export const VisibilityIcon: React.FC<{ visibility: StatusVisibility }> = ({
visibility, visibility,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();

View file

@ -8,7 +8,6 @@ import {
} from '../actions/accounts'; } from '../actions/accounts';
import { showAlertForError } from '../actions/alerts'; import { showAlertForError } from '../actions/alerts';
import { initBlockModal } from '../actions/blocks'; import { initBlockModal } from '../actions/blocks';
import { initBoostModal } from '../actions/boosts';
import { import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
@ -107,7 +106,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
if ((e && e.shiftKey) || !boostModal) { if ((e && e.shiftKey) || !boostModal) {
this.onModalReblog(status); this.onModalReblog(status);
} else { } else {
dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
} }
}, },
@ -262,7 +261,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}, },
deployPictureInPicture (status, type, mediaProps) { deployPictureInPicture (status, type, mediaProps) {
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps)); dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps}));
}, },
onInteractionModal (type, status) { onInteractionModal (type, status) {

View file

@ -22,23 +22,23 @@ describe('emoji', () => {
it('does unicode', () => { it('does unicode', () => {
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual( expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
'<img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">'); '<picture><img draggable="false" class="emojione" alt="👩‍👩‍👦‍👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg"></picture>');
expect(emojify('👨‍👩‍👧‍👧')).toEqual( expect(emojify('👨‍👩‍👧‍👧')).toEqual(
'<img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">'); '<picture><img draggable="false" class="emojione" alt="👨‍👩‍👧‍👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg"></picture>');
expect(emojify('👩‍👩‍👦')).toEqual('<img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg">'); expect(emojify('👩‍👩‍👦')).toEqual('<picture><img draggable="false" class="emojione" alt="👩‍👩‍👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg"></picture>');
expect(emojify('\u2757')).toEqual( expect(emojify('\u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">'); '<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
}); });
it('does multiple unicode', () => { it('does multiple unicode', () => {
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual( expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">'); '<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual( expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">'); '<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture><picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual( expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">'); '<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual( expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> bar'); 'foo <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> bar');
}); });
it('ignores unicode inside of tags', () => { it('ignores unicode inside of tags', () => {
@ -46,16 +46,16 @@ describe('emoji', () => {
}); });
it('does multiple emoji properly (issue 5188)', () => { it('does multiple emoji properly (issue 5188)', () => {
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">'); expect(emojify('👌🌈💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture><picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">'); expect(emojify('👌 🌈 💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture> <picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture> <picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
}); });
it('does an emoji that has no shortcode', () => { it('does an emoji that has no shortcode', () => {
expect(emojify('👁‍🗨')).toEqual('<img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg">'); expect(emojify('👁‍🗨')).toEqual('<picture><img draggable="false" class="emojione" alt="👁‍🗨" title="" src="/emoji/1f441-200d-1f5e8.svg"></picture>');
}); });
it('does an emoji whose filename is irregular', () => { it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg">'); expect(emojify('↙️')).toEqual('<picture><img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg"></picture>');
}); });
it('avoid emojifying on invisible text', () => { it('avoid emojifying on invisible text', () => {
@ -67,11 +67,11 @@ describe('emoji', () => {
it('avoid emojifying on invisible text with nested tags', () => { it('avoid emojifying on invisible text with nested tags', () => {
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇')) expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">'); .toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇')) expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">'); .toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
expect(emojify('<span class="invisible">😄<br>😴</span>😇')) expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
.toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">'); .toEqual('<span class="invisible">😄<br>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
}); });
it('does not emojify emojis with textual presentation VS15 character', () => { it('does not emojify emojis with textual presentation VS15 character', () => {
@ -79,19 +79,19 @@ describe('emoji', () => {
.toEqual('✴︎'); .toEqual('✴︎');
}); });
it('does an simple emoji properly', () => { it('does a simple emoji properly', () => {
expect(emojify('♀♂')) expect(emojify('♀♂'))
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg">'); .toEqual('<picture><img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"></picture><picture><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg"></picture>');
}); });
it('does an emoji containing ZWJ properly', () => { it('does an emoji containing ZWJ properly', () => {
expect(emojify('💂‍♀️💂‍♂️')) expect(emojify('💂‍♀️💂‍♂️'))
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">'); .toEqual('<picture><img draggable="false" class="emojione" alt="💂\u200D♀" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"></picture><picture><img draggable="false" class="emojione" alt="💂\u200D♂" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg"></picture>');
}); });
it('keeps ordering as expected (issue fixed by PR 20677)', () => { it('keeps ordering as expected (issue fixed by PR 20677)', () => {
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>')) expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'))
.toEqual('<p><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'); .toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>');
}); });
}); });
}); });

View file

@ -17,8 +17,13 @@ const emojiFilenames = (emojis) => {
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺', '📱', '📲', '🚲', '🪮', '🐦‍⬛']); const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂‍♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂‍♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺', '📱', '📲', '🚲', '🪮', '🐦‍⬛']);
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️', '🪽', '🪿']); const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️', '🪽', '🪿']);
const emojiFilename = (filename) => { /**
const borderedEmoji = (document.body && document.body.classList.contains('theme-mastodon-light')) ? lightEmoji : darkEmoji; * @param {string} filename
* @param {"light" | "dark" } colorScheme
* @returns {string}
*/
const emojiFilename = (filename, colorScheme) => {
const borderedEmoji = colorScheme === "light" ? lightEmoji : darkEmoji;
return borderedEmoji.includes(filename) ? (filename + '_border') : filename; return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
}; };
@ -92,12 +97,30 @@ const emojifyTextNode = (node, customEmojis) => {
const { filename, shortCode } = unicodeMapping[unicode_emoji]; const { filename, shortCode } = unicodeMapping[unicode_emoji];
const title = shortCode ? `:${shortCode}:` : ''; const title = shortCode ? `:${shortCode}:` : '';
replacement = document.createElement('img'); replacement = document.createElement('picture');
replacement.setAttribute('draggable', 'false');
replacement.setAttribute('class', 'emojione'); const isSystemTheme = !!document.body?.classList.contains('theme-system');
replacement.setAttribute('alt', unicode_emoji);
replacement.setAttribute('title', title); if(isSystemTheme) {
replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`); let source = document.createElement('source');
source.setAttribute('media', '(prefers-color-scheme: dark)');
source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`);
replacement.appendChild(source);
}
let img = document.createElement('img');
img.setAttribute('draggable', 'false');
img.setAttribute('class', 'emojione');
img.setAttribute('alt', unicode_emoji);
img.setAttribute('title', title);
let theme = "light";
if(!isSystemTheme && !document.body?.classList.contains('theme-mastodon-light'))
theme = "dark";
img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`);
replacement.appendChild(img);
} }
// Add the processed-up-to-now string and the emoji replacement // Add the processed-up-to-now string and the emoji replacement

View file

@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
import { dismissSuggestion } from 'mastodon/actions/suggestions';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import { domain } from 'mastodon/initial_state';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" },
});
export const Card = ({ id, source }) => {
const intl = useIntl();
const account = useSelector(state => state.getIn(['accounts', id]));
const relationship = useSelector(state => state.getIn(['relationships', id]));
const dispatch = useDispatch();
const following = relationship?.get('following') ?? relationship?.get('requested');
const handleFollow = useCallback(() => {
if (following) {
dispatch(unfollowAccount(id));
} else {
dispatch(followAccount(id));
}
}, [id, following, dispatch]);
const handleDismiss = useCallback(() => {
dispatch(dismissSuggestion(id));
}, [id, dispatch]);
let label;
switch (source) {
case 'friends_of_friends':
label = <FormattedMessage id='follow_suggestions.friends_of_friends_longer' defaultMessage='Popular among people you follow' />;
break;
case 'similar_to_recently_followed':
label = <FormattedMessage id='follow_suggestions.similar_to_recently_followed_longer' defaultMessage='Similar to profiles you recently followed' />;
break;
case 'featured':
label = <FormattedMessage id='follow_suggestions.featured_longer' defaultMessage='Hand-picked by the {domain} team' values={{ domain }} />;
break;
case 'most_followed':
label = <FormattedMessage id='follow_suggestions.popular_suggestion_longer' defaultMessage='Popular on {domain}' values={{ domain }} />;
break;
case 'most_interactions':
label = <FormattedMessage id='follow_suggestions.popular_suggestion_longer' defaultMessage='Popular on {domain}' values={{ domain }} />;
break;
}
return (
<div className='explore__suggestions__card'>
<div className='explore__suggestions__card__source'>
{label}
</div>
<div className='explore__suggestions__card__body'>
<Link to={`/@${account.get('acct')}`}><Avatar account={account} size={48} /></Link>
<div className='explore__suggestions__card__body__main'>
<div className='explore__suggestions__card__body__main__name-button'>
<Link className='explore__suggestions__card__body__main__name-button__name' to={`/@${account.get('acct')}`}><DisplayName account={account} /></Link>
<IconButton iconComponent={CloseIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} secondary={following} onClick={handleFollow} />
</div>
</div>
</div>
</div>
);
};
Card.propTypes = {
id: PropTypes.string.isRequired,
source: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']),
};

View file

@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions'; import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import AccountCard from 'mastodon/features/directory/components/account_card';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Card } from './components/card';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']), suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']), isLoading: state.getIn(['suggestions', 'isLoading']),
@ -54,7 +55,11 @@ class Suggestions extends PureComponent {
return ( return (
<div className='explore__suggestions scrollable' data-nosnippet> <div className='explore__suggestions scrollable' data-nosnippet>
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => ( {isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} /> <Card
key={suggestion.get('account')}
id={suggestion.get('account')}
source={suggestion.getIn(['sources', 0])}
/>
))} ))}
</div> </div>
); );

View file

@ -82,7 +82,7 @@ class GettingStarted extends ImmutablePureComponent {
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.map, myAccount: ImmutablePropTypes.record,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
fetchFollowRequests: PropTypes.func.isRequired, fetchFollowRequests: PropTypes.func.isRequired,
unreadFollowRequests: PropTypes.number, unreadFollowRequests: PropTypes.number,

View file

@ -107,7 +107,7 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td> <td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
</tr> </tr>
<tr> <tr>
<td><kbd>s</kbd></td> <td><kbd>s</kbd>, <kbd>/</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td> <td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
</tr> </tr>
<tr> <tr>

View file

@ -27,7 +27,7 @@ export const FilteredNotificationsBanner = () => {
}; };
}, [dispatch]); }, [dispatch]);
if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) * 1 === 0) { if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) === 0) {
return null; return null;
} }
@ -41,7 +41,8 @@ export const FilteredNotificationsBanner = () => {
</div> </div>
<div className='filtered-notifications-banner__badge'> <div className='filtered-notifications-banner__badge'>
{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))} <div className='filtered-notifications-banner__badge__badge'>{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}</div>
<FormattedMessage id='filtered_notifications_banner.mentions' defaultMessage='{count, plural, one {mention} other {mentions}}' values={{ count: policy.getIn(['summary', 'pending_notifications_count']) }} />
</div> </div>
</Link> </Link>
); );

View file

@ -0,0 +1,78 @@
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import WarningIcon from '@/material-icons/400-24px/warning-fill.svg?react';
import { Icon } from 'mastodon/components/icon';
// This needs to be kept in sync with app/models/account_warning.rb
const messages = defineMessages({
none: {
id: 'notification.moderation_warning.action_none',
defaultMessage: 'Your account has received a moderation warning.',
},
disable: {
id: 'notification.moderation_warning.action_disable',
defaultMessage: 'Your account has been disabled.',
},
mark_statuses_as_sensitive: {
id: 'notification.moderation_warning.action_mark_statuses_as_sensitive',
defaultMessage: 'Some of your posts have been marked as sensitive.',
},
delete_statuses: {
id: 'notification.moderation_warning.action_delete_statuses',
defaultMessage: 'Some of your posts have been removed.',
},
sensitive: {
id: 'notification.moderation_warning.action_sensitive',
defaultMessage: 'Your posts will be marked as sensitive from now on.',
},
silence: {
id: 'notification.moderation_warning.action_silence',
defaultMessage: 'Your account has been limited.',
},
suspend: {
id: 'notification.moderation_warning.action_suspend',
defaultMessage: 'Your account has been suspended.',
},
});
interface Props {
action:
| 'none'
| 'disable'
| 'mark_statuses_as_sensitive'
| 'delete_statuses'
| 'sensitive'
| 'silence'
| 'suspend';
id: string;
hidden: boolean;
}
export const ModerationWarning: React.FC<Props> = ({ action, id, hidden }) => {
const intl = useIntl();
if (hidden) {
return null;
}
return (
<a
href={`/disputes/strikes/${id}`}
target='_blank'
rel='noopener noreferrer'
className='notification__moderation-warning'
>
<Icon id='warning' icon={WarningIcon} />
<div className='notification__moderation-warning__content'>
<p>{intl.formatMessage(messages[action])}</p>
<span className='link-button'>
<FormattedMessage
id='notification.moderation-warning.learn_more'
defaultMessage='Learn more'
/>
</span>
</div>
</a>
);
};

View file

@ -12,7 +12,6 @@ import { HotKeys } from 'react-hotkeys';
import EditIcon from '@/material-icons/400-24px/edit.svg?react'; import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react'; import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react'; import PersonIcon from '@/material-icons/400-24px/person-fill.svg?react';
@ -27,7 +26,8 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import FollowRequestContainer from '../containers/follow_request_container'; import FollowRequestContainer from '../containers/follow_request_container';
import RelationshipsSeveranceEvent from './relationships_severance_event'; import { ModerationWarning } from './moderation_warning';
import { RelationshipsSeveranceEvent } from './relationships_severance_event';
import Report from './report'; import Report from './report';
const messages = defineMessages({ const messages = defineMessages({
@ -40,6 +40,8 @@ const messages = defineMessages({
update: { id: 'notification.update', defaultMessage: '{name} edited a post' }, update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' }, adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' }, adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
relationshipsSevered: { id: 'notification.relationships_severance_event', defaultMessage: 'Lost connections with {name}' },
moderationWarning: { id: 'notification.moderation_warning', defaultMessage: 'Your have received a moderation warning' },
}); });
const notificationForScreenReader = (intl, message, timestamp) => { const notificationForScreenReader = (intl, message, timestamp) => {
@ -361,24 +363,44 @@ class Notification extends ImmutablePureComponent {
} }
renderRelationshipsSevered (notification) { renderRelationshipsSevered (notification) {
const { intl, unread } = this.props; const { intl, unread, hidden } = this.props;
const event = notification.get('event');
if (!notification.get('event')) { if (!event) {
return null; return null;
} }
return ( return (
<HotKeys handlers={this.getHandlers()}> <HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-severed-relationships focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: notification.getIn(['event', 'target_name']) }), notification.get('created_at'))}> <div className={classNames('notification notification-severed-relationships focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.relationshipsSevered, { name: notification.getIn(['event', 'target_name']) }), notification.get('created_at'))}>
<div className='notification__message'> <RelationshipsSeveranceEvent
<Icon id='heart_broken' icon={HeartBrokenIcon} /> type={event.get('type')}
target={event.get('target_name')}
followersCount={event.get('followers_count')}
followingCount={event.get('following_count')}
hidden={hidden}
/>
</div>
</HotKeys>
);
}
<span title={notification.get('created_at')}> renderModerationWarning (notification) {
<FormattedMessage id='notification.severed_relationships' defaultMessage='Relationships with {name} severed' values={{ name: notification.getIn(['event', 'target_name']) }} /> const { intl, unread, hidden } = this.props;
</span> const warning = notification.get('moderation_warning');
</div>
<RelationshipsSeveranceEvent event={notification.get('event')} /> if (!warning) {
return null;
}
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-moderation-warning focusable', { unread })} tabIndex={0} aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.moderationWarning), notification.get('created_at'))}>
<ModerationWarning
action={warning.get('action')}
id={warning.get('id')}
hidden={hidden}
/>
</div> </div>
</HotKeys> </HotKeys>
); );
@ -457,6 +479,8 @@ class Notification extends ImmutablePureComponent {
return this.renderPoll(notification, account); return this.renderPoll(notification, account);
case 'severed_relationships': case 'severed_relationships':
return this.renderRelationshipsSevered(notification); return this.renderRelationshipsSevered(notification);
case 'moderation_warning':
return this.renderModerationWarning(notification);
case 'admin.sign_up': case 'admin.sign_up':
return this.renderAdminSignUp(notification, account, link); return this.renderAdminSignUp(notification, account, link);
case 'admin.report': case 'admin.report':

View file

@ -7,8 +7,8 @@ import { Link } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import DoneIcon from '@/material-icons/400-24px/done.svg?react'; import DoneIcon from '@/material-icons/400-24px/done.svg?react';
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications'; import { acceptNotificationRequest, dismissNotificationRequest } from 'mastodon/actions/notifications';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
@ -51,7 +51,7 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
</Link> </Link>
<div className='notification-request__actions'> <div className='notification-request__actions'>
<IconButton iconComponent={VolumeOffIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} /> <IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
<IconButton iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} /> <IconButton iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
</div> </div>
</div> </div>

View file

@ -2,60 +2,44 @@ import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes'; import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
import { Icon } from 'mastodon/components/icon';
import { domain } from 'mastodon/initial_state';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; // This needs to be kept in sync with app/models/relationships_severance_event.rb
// This needs to be kept in sync with app/models/relationship_severance_event.rb
const messages = defineMessages({ const messages = defineMessages({
account_suspension: { id: 'relationship_severance_notification.types.account_suspension', defaultMessage: 'Account has been suspended' }, account_suspension: { id: 'notification.relationships_severance_event.account_suspension', defaultMessage: 'An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.' },
domain_block: { id: 'relationship_severance_notification.types.domain_block', defaultMessage: 'Domain has been suspended' }, domain_block: { id: 'notification.relationships_severance_event.domain_block', defaultMessage: 'An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
user_domain_block: { id: 'relationship_severance_notification.types.user_domain_block', defaultMessage: 'You blocked this domain' }, user_domain_block: { id: 'notification.relationships_severance_event.user_domain_block', defaultMessage: 'You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
}); });
const RelationshipsSeveranceEvent = ({ event, hidden }) => { export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden }) => {
const intl = useIntl(); const intl = useIntl();
if (hidden || !event) { if (hidden) {
return null; return null;
} }
return ( return (
<div className='notification__report'> <a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='notification__relationships-severance-event'>
<div className='notification__report__details'> <Icon id='heart_broken' icon={HeartBrokenIcon} />
<div>
<RelativeTimestamp timestamp={event.get('created_at')} short={false} />
{' · '}
{ event.get('purged') ? (
<FormattedMessage
id='relationship_severance_notification.purged_data'
defaultMessage='purged by administrators'
/>
) : (
<FormattedMessage
id='relationship_severance_notification.relationships'
defaultMessage='{count, plural, one {# relationship} other {# relationships}}'
values={{ count: event.get('followers_count', 0) + event.get('following_count', 0) }}
/>
)}
<br />
<strong>{intl.formatMessage(messages[event.get('type')])}</strong>
</div>
<div className='notification__report__actions'> <div className='notification__relationships-severance-event__content'>
<a href='/severed_relationships' className='button' target='_blank' rel='noopener noreferrer'> <p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p>
<FormattedMessage id='relationship_severance_notification.view' defaultMessage='View' /> <span className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></span>
</a>
</div>
</div> </div>
</div> </a>
); );
}; };
RelationshipsSeveranceEvent.propTypes = { RelationshipsSeveranceEvent.propTypes = {
event: ImmutablePropTypes.map.isRequired, type: PropTypes.oneOf([
'account_suspension',
'domain_block',
'user_domain_block',
]).isRequired,
target: PropTypes.string.isRequired,
followersCount: PropTypes.number.isRequired,
followingCount: PropTypes.number.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
}; };
export default RelationshipsSeveranceEvent;

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { initBoostModal } from '../../../actions/boosts';
import { mentionCompose } from '../../../actions/compose'; import { mentionCompose } from '../../../actions/compose';
import { import {
reblog, reblog,
@ -8,6 +7,7 @@ import {
unreblog, unreblog,
unfavourite, unfavourite,
} from '../../../actions/interactions'; } from '../../../actions/interactions';
import { openModal } from '../../../actions/modal';
import { import {
hideStatus, hideStatus,
revealStatus, revealStatus,
@ -49,7 +49,7 @@ const mapDispatchToProps = dispatch => ({
if (e.shiftKey || !boostModal) { if (e.shiftKey || !boostModal) {
this.onModalReblog(status); this.onModalReblog(status);
} else { } else {
dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
} }
} }
}, },

View file

@ -16,6 +16,7 @@ import EditNoteIcon from '@/material-icons/400-24px/edit_note.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import { focusCompose } from 'mastodon/actions/compose'; import { focusCompose } from 'mastodon/actions/compose';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import Column from 'mastodon/features/ui/components/column'; import Column from 'mastodon/features/ui/components/column';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
@ -42,42 +43,44 @@ const Onboarding = () => {
return ( return (
<Column> <Column>
<Switch> {account ? (
<Route path='/start' exact> <Switch>
<div className='scrollable privacy-policy'> <Route path='/start' exact>
<div className='column-title'> <div className='scrollable privacy-policy'>
<img src={illustration} alt='' className='onboarding__illustration' /> <div className='column-title'>
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3> <img src={illustration} alt='' className='onboarding__illustration' />
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p> <h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
</div>
<div className='onboarding__steps'>
<Step to='/start/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
<Step to='/start/follows' completed={(account.get('following_count') * 1) >= 1} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
<Step onClick={handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
<Step to='/start/share' icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
<div className='onboarding__links'>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
</div> </div>
</Route>
<div className='onboarding__steps'> <Route path='/start/profile' component={Profile} />
<Step to='/start/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} /> <Route path='/start/follows' component={Follows} />
<Step to='/start/follows' completed={(account.get('following_count') * 1) >= 1} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} /> <Route path='/start/share' component={Share} />
<Step onClick={handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} /> </Switch>
<Step to='/start/share' icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} /> ) : <NotSignedInIndicator />}
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
<div className='onboarding__links'>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
</div>
</Route>
<Route path='/start/profile' component={Profile} />
<Route path='/start/follows' component={Follows} />
<Route path='/start/share' component={Share} />
</Switch>
<Helmet> <Helmet>
<meta name='robots' content='noindex' /> <meta name='robots' content='noindex' />

View file

@ -14,7 +14,6 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react'; import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { initBoostModal } from 'mastodon/actions/boosts';
import { replyCompose } from 'mastodon/actions/compose'; import { replyCompose } from 'mastodon/actions/compose';
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions'; import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
@ -140,7 +139,7 @@ class Footer extends ImmutablePureComponent {
} else if ((e && e.shiftKey) || !boostModal) { } else if ((e && e.shiftKey) || !boostModal) {
this._performReblog(status); this._performReblog(status);
} else { } else {
dispatch(initBoostModal({ status, onReblog: this._performReblog })); dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this._performReblog } }));
} }
} else { } else {
dispatch(openModal({ dispatch(openModal({
@ -210,4 +209,4 @@ class Footer extends ImmutablePureComponent {
} }
export default withRouter(connect(makeMapStateToProps)(injectIntl(Footer))); export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer)));

View file

@ -1,51 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
const mapStateToProps = (state, { accountId }) => ({
account: state.getIn(['accounts', accountId]),
});
class Header extends ImmutablePureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
statusId: PropTypes.string.isRequired,
account: ImmutablePropTypes.record.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { account, statusId, onClose, intl } = this.props;
return (
<div className='picture-in-picture__header'>
<Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'>
<Avatar account={account} size={36} />
<DisplayName account={account} />
</Link>
<IconButton icon='times' iconComponent={CloseIcon} onClick={onClose} title={intl.formatMessage(messages.close)} />
</div>
);
}
}
export default connect(mapStateToProps)(injectIntl(Header));

View file

@ -0,0 +1,46 @@
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { IconButton } from 'mastodon/components/icon_button';
import { useAppSelector } from 'mastodon/store';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
});
interface Props {
accountId: string;
statusId: string;
onClose: () => void;
}
export const Header: React.FC<Props> = ({ accountId, statusId, onClose }) => {
const account = useAppSelector((state) => state.accounts.get(accountId));
const intl = useIntl();
if (!account) return null;
return (
<div className='picture-in-picture__header'>
<Link
to={`/@${account.get('acct')}/${statusId}`}
className='picture-in-picture__header__account'
>
<Avatar account={account} size={36} />
<DisplayName account={account} />
</Link>
<IconButton
icon='times'
iconComponent={CloseIcon}
onClick={onClose}
title={intl.formatMessage(messages.close)}
/>
</div>
);
};

View file

@ -1,89 +0,0 @@
import PropTypes from 'prop-types';
import { Component } from 'react';
import { connect } from 'react-redux';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import Audio from 'mastodon/features/audio';
import Video from 'mastodon/features/video';
import Footer from './components/footer';
import Header from './components/header';
const mapStateToProps = state => ({
...state.get('picture_in_picture'),
});
class PictureInPicture extends Component {
static propTypes = {
statusId: PropTypes.string,
accountId: PropTypes.string,
type: PropTypes.string,
src: PropTypes.string,
muted: PropTypes.bool,
volume: PropTypes.number,
currentTime: PropTypes.number,
poster: PropTypes.string,
backgroundColor: PropTypes.string,
foregroundColor: PropTypes.string,
accentColor: PropTypes.string,
dispatch: PropTypes.func.isRequired,
};
handleClose = () => {
const { dispatch } = this.props;
dispatch(removePictureInPicture());
};
render () {
const { type, src, currentTime, accountId, statusId } = this.props;
if (!currentTime) {
return null;
}
let player;
if (type === 'video') {
player = (
<Video
src={src}
currentTime={this.props.currentTime}
volume={this.props.volume}
muted={this.props.muted}
autoPlay
inline
alwaysVisible
/>
);
} else if (type === 'audio') {
player = (
<Audio
src={src}
currentTime={this.props.currentTime}
volume={this.props.volume}
muted={this.props.muted}
poster={this.props.poster}
backgroundColor={this.props.backgroundColor}
foregroundColor={this.props.foregroundColor}
accentColor={this.props.accentColor}
autoPlay
/>
);
}
return (
<div className='picture-in-picture'>
<Header accountId={accountId} statusId={statusId} onClose={this.handleClose} />
{player}
<Footer statusId={statusId} />
</div>
);
}
}
export default connect(mapStateToProps)(PictureInPicture);

View file

@ -0,0 +1,79 @@
import { useCallback } from 'react';
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
import Audio from 'mastodon/features/audio';
import Video from 'mastodon/features/video';
import { useAppDispatch, useAppSelector } from 'mastodon/store/typed_functions';
import Footer from './components/footer';
import { Header } from './components/header';
export const PictureInPicture: React.FC = () => {
const dispatch = useAppDispatch();
const handleClose = useCallback(() => {
dispatch(removePictureInPicture());
}, [dispatch]);
const pipState = useAppSelector((s) => s.picture_in_picture);
if (pipState.type === null) {
return null;
}
const {
type,
src,
currentTime,
accountId,
statusId,
volume,
muted,
poster,
backgroundColor,
foregroundColor,
accentColor,
} = pipState;
let player;
switch (type) {
case 'video':
player = (
<Video
src={src}
currentTime={currentTime}
volume={volume}
muted={muted}
autoPlay
inline
alwaysVisible
/>
);
break;
case 'audio':
player = (
<Audio
src={src}
currentTime={currentTime}
volume={volume}
muted={muted}
poster={poster}
backgroundColor={backgroundColor}
foregroundColor={foregroundColor}
accentColor={accentColor}
autoPlay
/>
);
}
return (
<div className='picture-in-picture'>
<Header accountId={accountId} statusId={statusId} onClose={handleClose} />
{player}
<Footer statusId={statusId} />
</div>
);
};

View file

@ -74,7 +74,7 @@ class ActionBar extends PureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.map, relationship: ImmutablePropTypes.record,
onReply: PropTypes.func.isRequired, onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired, onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired, onFavourite: PropTypes.func.isRequired,

View file

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { showAlertForError } from '../../../actions/alerts'; import { showAlertForError } from '../../../actions/alerts';
import { initBlockModal } from '../../../actions/blocks'; import { initBlockModal } from '../../../actions/blocks';
import { initBoostModal } from '../../../actions/boosts';
import { import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
@ -85,7 +84,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (e.shiftKey || !boostModal) { if (e.shiftKey || !boostModal) {
this.onModalReblog(status); this.onModalReblog(status);
} else { } else {
dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
} }
} }
}, },

View file

@ -27,7 +27,6 @@ import {
unmuteAccount, unmuteAccount,
} from '../../actions/accounts'; } from '../../actions/accounts';
import { initBlockModal } from '../../actions/blocks'; import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts';
import { import {
replyCompose, replyCompose,
mentionCompose, mentionCompose,
@ -317,7 +316,7 @@ class Status extends ImmutablePureComponent {
if ((e && e.shiftKey) || !boostModal) { if ((e && e.shiftKey) || !boostModal) {
this.handleModalReblog(status); this.handleModalReblog(status);
} else { } else {
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.handleModalReblog } }));
} }
} }
} else { } else {

View file

@ -1,125 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { withRouter } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { changeBoostPrivacy } from 'mastodon/actions/boosts';
import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon';
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { Avatar } from '../../../components/avatar';
import { Button } from '../../../components/button';
import { DisplayName } from '../../../components/display_name';
import { RelativeTimestamp } from '../../../components/relative_timestamp';
import StatusContent from '../../../components/status_content';
const messages = defineMessages({
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
});
const mapStateToProps = state => {
return {
privacy: state.getIn(['boosts', 'new', 'privacy']),
};
};
const mapDispatchToProps = dispatch => {
return {
onChangeBoostPrivacy(value) {
dispatch(changeBoostPrivacy(value));
},
};
};
class BoostModal extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
onReblog: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onChangeBoostPrivacy: PropTypes.func.isRequired,
privacy: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
...WithRouterPropTypes,
};
handleReblog = () => {
this.props.onReblog(this.props.status, this.props.privacy);
this.props.onClose();
};
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.onClose();
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
}
};
_findContainer = () => {
return document.getElementsByClassName('modal-root__container')[0];
};
render () {
const { status, privacy, intl } = this.props;
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
return (
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
<div className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
<RelativeTimestamp timestamp={status.get('created_at')} />
</a>
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name'>
<div className='status__avatar'>
<Avatar account={status.get('account')} size={48} />
</div>
<DisplayName account={status.get('account')} />
</a>
</div>
<StatusContent status={status} />
{status.get('media_attachments').size > 0 && (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
)}
</div>
</div>
<div className='boost-modal__action-bar'>
<div><FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' icon={RepeatIcon} /></span> }} /></div>
{status.get('visibility') !== 'private' && !status.get('reblogged') && (
<PrivacyDropdown
noDirect
value={privacy}
container={this._findContainer}
onChange={this.props.onChangeBoostPrivacy}
/>
)}
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} autoFocus />
</div>
</div>
);
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal)));

View file

@ -0,0 +1,162 @@
import type { MouseEventHandler } from 'react';
import { useCallback, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router';
import type Immutable from 'immutable';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import AttachmentList from 'mastodon/components/attachment_list';
import { Icon } from 'mastodon/components/icon';
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown';
import type { Account } from 'mastodon/models/account';
import type { Status, StatusVisibility } from 'mastodon/models/status';
import { useAppSelector } from 'mastodon/store';
import { Avatar } from '../../../components/avatar';
import { Button } from '../../../components/button';
import { DisplayName } from '../../../components/display_name';
import { RelativeTimestamp } from '../../../components/relative_timestamp';
import StatusContent from '../../../components/status_content';
const messages = defineMessages({
cancel_reblog: {
id: 'status.cancel_reblog_private',
defaultMessage: 'Unboost',
},
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
});
export const BoostModal: React.FC<{
status: Status;
onClose: () => void;
onReblog: (status: Status, privacy: StatusVisibility) => void;
}> = ({ status, onReblog, onClose }) => {
const intl = useIntl();
const history = useHistory();
const default_privacy = useAppSelector(
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
(state) => state.compose.get('default_privacy') as StatusVisibility,
);
const account = status.get('account') as Account;
const statusVisibility = status.get('visibility') as StatusVisibility;
const [privacy, setPrivacy] = useState<StatusVisibility>(
statusVisibility === 'private' ? 'private' : default_privacy,
);
const onPrivacyChange = useCallback((value: StatusVisibility) => {
setPrivacy(value);
}, []);
const handleReblog = useCallback(() => {
onReblog(status, privacy);
onClose();
}, [onClose, onReblog, status, privacy]);
const handleAccountClick = useCallback<MouseEventHandler>(
(e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
onClose();
history.push(`/@${account.acct}`);
}
},
[history, onClose, account],
);
const buttonText = status.get('reblogged')
? messages.cancel_reblog
: messages.reblog;
const findContainer = useCallback(
() => document.getElementsByClassName('modal-root__container')[0],
[],
);
return (
<div className='modal-root__modal boost-modal'>
<div className='boost-modal__container'>
<div
className={classNames(
'status',
`status-${statusVisibility}`,
'light',
)}
>
<div className='status__info'>
<a
href={`/@${account.acct}/${status.get('id') as string}`}
className='status__relative-time'
target='_blank'
rel='noopener noreferrer'
>
<span className='status__visibility-icon'>
<VisibilityIcon visibility={statusVisibility} />
</span>
<RelativeTimestamp
timestamp={status.get('created_at') as string}
/>
</a>
<a
onClick={handleAccountClick}
href={`/@${account.acct}`}
className='status__display-name'
>
<div className='status__avatar'>
<Avatar account={account} size={48} />
</div>
<DisplayName account={account} />
</a>
</div>
{/* @ts-expect-error Expected until StatusContent is typed */}
<StatusContent status={status} />
{(status.get('media_attachments') as Immutable.List<unknown>).size >
0 && (
<AttachmentList compact media={status.get('media_attachments')} />
)}
</div>
</div>
<div className='boost-modal__action-bar'>
<div>
<FormattedMessage
id='boost_modal.combo'
defaultMessage='You can press {combo} to skip this next time'
values={{
combo: (
<span>
Shift + <Icon id='retweet' icon={RepeatIcon} />
</span>
),
}}
/>
</div>
{statusVisibility !== 'private' && !status.get('reblogged') && (
<PrivacyDropdown
noDirect
value={privacy}
container={findContainer}
onChange={onPrivacyChange}
/>
)}
<Button
text={intl.formatMessage(buttonText)}
onClick={handleReblog}
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
/>
</div>
</div>
);
};

View file

@ -24,7 +24,7 @@ import BundleContainer from '../containers/bundle_container';
import ActionsModal from './actions_modal'; import ActionsModal from './actions_modal';
import AudioModal from './audio_modal'; import AudioModal from './audio_modal';
import BoostModal from './boost_modal'; import { BoostModal } from './boost_modal';
import BundleModalError from './bundle_modal_error'; import BundleModalError from './bundle_modal_error';
import ConfirmationModal from './confirmation_modal'; import ConfirmationModal from './confirmation_modal';
import FocalPointModal from './focal_point_modal'; import FocalPointModal from './focal_point_modal';

View file

@ -14,7 +14,7 @@ import { HotKeys } from 'react-hotkeys';
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
import PictureInPicture from 'mastodon/features/picture_in_picture'; import { PictureInPicture } from 'mastodon/features/picture_in_picture';
import { layoutFromWindow } from 'mastodon/is_mobile'; import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -89,7 +89,7 @@ const mapStateToProps = state => ({
const keyMap = { const keyMap = {
help: '?', help: '?',
new: 'n', new: 'n',
search: 's', search: ['s', '/'],
forceNew: 'option+n', forceNew: 'option+n',
toggleComposeSpoilers: 'option+x', toggleComposeSpoilers: 'option+x',
focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'], focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],

View file

@ -89,6 +89,14 @@
"announcement.announcement": "إعلان", "announcement.announcement": "إعلان",
"attachments_list.unprocessed": "(غير معالَج)", "attachments_list.unprocessed": "(غير معالَج)",
"audio.hide": "إخفاء المقطع الصوتي", "audio.hide": "إخفاء المقطع الصوتي",
"block_modal.remote_users_caveat": "Do ti kërkojmë shërbyesit {domain} të respektojë vendimin tuaj. Por, pajtimi sështë i garantuar, ngaqë disa shërbyes mund ti trajtojnë ndryshe bllokimet. Psotimet publike mundet të jenë ende të dukshme për përdorues pa bërë hyrje në llogari.",
"block_modal.show_less": "اعرض أقلّ",
"block_modal.show_more": "أظهر المزيد",
"block_modal.they_cant_mention": "لن يستطيع ذِكرك أو متابعتك.",
"block_modal.they_cant_see_posts": "لن يستطيع رؤية منشوراتك ولن ترى منشوراته.",
"block_modal.they_will_know": "يمكنه أن يرى أنه قد تم حجبه.",
"block_modal.title": "أتريد حظر المستخدم؟",
"block_modal.you_wont_see_mentions": "لن تر المنشورات التي يُشار فيهم إليه.",
"boost_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة", "boost_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة",
"bundle_column_error.copy_stacktrace": "انسخ تقرير الخطأ", "bundle_column_error.copy_stacktrace": "انسخ تقرير الخطأ",
"bundle_column_error.error.body": "لا يمكن تقديم الصفحة المطلوبة. قد يكون بسبب خطأ في التعليمات البرمجية، أو مشكلة توافق المتصفح.", "bundle_column_error.error.body": "لا يمكن تقديم الصفحة المطلوبة. قد يكون بسبب خطأ في التعليمات البرمجية، أو مشكلة توافق المتصفح.",
@ -169,6 +177,7 @@
"confirmations.delete_list.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَذفَ هذِهِ القائمة بشكلٍ دائم؟", "confirmations.delete_list.message": "هل أنتَ مُتأكدٌ أنكَ تُريدُ حَذفَ هذِهِ القائمة بشكلٍ دائم؟",
"confirmations.discard_edit_media.confirm": "تجاهل", "confirmations.discard_edit_media.confirm": "تجاهل",
"confirmations.discard_edit_media.message": "لديك تغييرات غير محفوظة لوصف الوسائط أو معاينتها، أتريد تجاهلها على أي حال؟", "confirmations.discard_edit_media.message": "لديك تغييرات غير محفوظة لوصف الوسائط أو معاينتها، أتريد تجاهلها على أي حال؟",
"confirmations.domain_block.confirm": "حظر الخادم",
"confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.", "confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.",
"confirmations.edit.confirm": "تعديل", "confirmations.edit.confirm": "تعديل",
"confirmations.edit.message": "التعديل في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد تحريرها. متأكد من أنك تريد المواصلة؟", "confirmations.edit.message": "التعديل في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد تحريرها. متأكد من أنك تريد المواصلة؟",
@ -200,6 +209,23 @@
"dismissable_banner.explore_statuses": "هذه هي المنشورات الرائجة على الشبكات الاجتماعيّة اليوم. تظهر المنشورات المعاد نشرها والحائزة على مفضّلات أكثر في مرتبة عليا.", "dismissable_banner.explore_statuses": "هذه هي المنشورات الرائجة على الشبكات الاجتماعيّة اليوم. تظهر المنشورات المعاد نشرها والحائزة على مفضّلات أكثر في مرتبة عليا.",
"dismissable_banner.explore_tags": "هذه هي الوسوم تكتسب جذب الاهتمام حاليًا على الويب الاجتماعي. الوسوم التي يستخدمها مختلف الناس تحتل مرتبة عليا.", "dismissable_banner.explore_tags": "هذه هي الوسوم تكتسب جذب الاهتمام حاليًا على الويب الاجتماعي. الوسوم التي يستخدمها مختلف الناس تحتل مرتبة عليا.",
"dismissable_banner.public_timeline": "هذه هي أحدث المنشورات العامة من الناس على الشبكة الاجتماعية التي يتبعها الناس على {domain}.", "dismissable_banner.public_timeline": "هذه هي أحدث المنشورات العامة من الناس على الشبكة الاجتماعية التي يتبعها الناس على {domain}.",
"domain_block_modal.block": "حظر الخادم",
"domain_block_modal.block_account_instead": "أحجب @{name} بدلاً من ذلك",
"domain_block_modal.they_can_interact_with_old_posts": "يمكن للأشخاص من هذا الخادم التفاعل مع منشوراتك القديمة.",
"domain_block_modal.they_cant_follow": "لا أحد من هذا الخادم يمكنه متابعتك.",
"domain_block_modal.they_wont_know": "لن يَعرف أنه قد تم حظره.",
"domain_block_modal.title": "أتريد حظر النطاق؟",
"domain_block_modal.you_will_lose_followers": "سيتم إزالة جميع متابعيك من هذا الخادم.",
"domain_block_modal.you_wont_see_posts": "لن ترى منشورات أو إشعارات من المستخدمين على هذا الخادم.",
"domain_pill.activitypub_lets_connect": "يتيح لك التواصل والتفاعل مع الناس ليس فقط على ماستدون، ولكن عبر تطبيقات اجتماعية مختلفة أيضا.",
"domain_pill.activitypub_like_language": "إنّ ActivityPub مثل لغة ماستدون التي يتحدث بها مع شبكات اجتماعية أخرى.",
"domain_pill.server": "الخادِم",
"domain_pill.their_handle": "مُعرِّفُه:",
"domain_pill.their_server": "بيتهم الرقمي، حيث تُستضاف كافة منشوراتهم.",
"domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.",
"domain_pill.username": "اسم المستخدم",
"domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟",
"domain_pill.your_handle": "عنوانك الكامل:",
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.", "embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
"embed.preview": "إليك ما سيبدو عليه:", "embed.preview": "إليك ما سيبدو عليه:",
"emoji_button.activity": "الأنشطة", "emoji_button.activity": "الأنشطة",
@ -266,6 +292,7 @@
"filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة", "filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة",
"filter_modal.select_filter.title": "تصفية هذا المنشور", "filter_modal.select_filter.title": "تصفية هذا المنشور",
"filter_modal.title.status": "تصفية منشور", "filter_modal.title.status": "تصفية منشور",
"filtered_notifications_banner.title": "الإشعارات المصفاة",
"firehose.all": "الكل", "firehose.all": "الكل",
"firehose.local": "هذا الخادم", "firehose.local": "هذا الخادم",
"firehose.remote": "خوادم أخرى", "firehose.remote": "خوادم أخرى",
@ -394,6 +421,13 @@
"loading_indicator.label": "جاري التحميل…", "loading_indicator.label": "جاري التحميل…",
"media_gallery.toggle_visible": "{number, plural, zero {} one {اخف الصورة} two {اخف الصورتين} few {اخف الصور} many {اخف الصور} other {اخف الصور}}", "media_gallery.toggle_visible": "{number, plural, zero {} one {اخف الصورة} two {اخف الصورتين} few {اخف الصور} many {اخف الصور} other {اخف الصور}}",
"moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.", "moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.",
"mute_modal.hide_options": "إخفاء الخيارات",
"mute_modal.show_options": "إظهار الخيارات",
"mute_modal.they_can_mention_and_follow": "سيكون بإمكانه الإشارة إليك ومتابعتك، لكنك لن تره.",
"mute_modal.they_wont_know": "لن يَعرف أنه قد تم كتمه.",
"mute_modal.title": "أتريد كتم المُستخدم؟",
"mute_modal.you_wont_see_mentions": "سوف لن تر المنشورات التي يُشار إليه.",
"mute_modal.you_wont_see_posts": "سيكون بإمكانه رؤية منشوراتك، لكنك لن ترى منشوراته.",
"navigation_bar.about": "عن", "navigation_bar.about": "عن",
"navigation_bar.advanced_interface": "افتحه في واجهة الويب المتقدمة", "navigation_bar.advanced_interface": "افتحه في واجهة الويب المتقدمة",
"navigation_bar.blocks": "الحسابات المحجوبة", "navigation_bar.blocks": "الحسابات المحجوبة",
@ -429,14 +463,21 @@
"notification.own_poll": "انتهى استطلاعك للرأي", "notification.own_poll": "انتهى استطلاعك للرأي",
"notification.poll": "لقد انتهى استطلاع رأي شاركتَ فيه", "notification.poll": "لقد انتهى استطلاع رأي شاركتَ فيه",
"notification.reblog": "قام {name} بمشاركة منشورك", "notification.reblog": "قام {name} بمشاركة منشورك",
"notification.relationships_severance_event.learn_more": "اعرف المزيد",
"notification.status": "{name} نشر للتو", "notification.status": "{name} نشر للتو",
"notification.update": "عدّلَ {name} منشورًا", "notification.update": "عدّلَ {name} منشورًا",
"notification_requests.accept": "موافقة",
"notification_requests.dismiss": "تخطي",
"notification_requests.notifications_from": "إشعارات من {name}",
"notification_requests.title": "الإشعارات المصفاة",
"notifications.clear": "مسح الإشعارات", "notifications.clear": "مسح الإشعارات",
"notifications.clear_confirmation": "متأكد من أنك تود مسح جميع الإشعارات الخاصة بك و المتلقاة إلى حد الآن ؟", "notifications.clear_confirmation": "متأكد من أنك تود مسح جميع الإشعارات الخاصة بك و المتلقاة إلى حد الآن ؟",
"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.favourite": "المفضلة:",
"notifications.column_settings.filter_bar.advanced": "عرض جميع الفئات",
"notifications.column_settings.filter_bar.category": "شريط التصفية السريعة",
"notifications.column_settings.follow": "متابعُون جُدُد:", "notifications.column_settings.follow": "متابعُون جُدُد:",
"notifications.column_settings.follow_request": "الطلبات الجديد لِمتابَعتك:", "notifications.column_settings.follow_request": "الطلبات الجديد لِمتابَعتك:",
"notifications.column_settings.mention": "الإشارات:", "notifications.column_settings.mention": "الإشارات:",
@ -462,6 +503,10 @@
"notifications.permission_denied": "تنبيهات سطح المكتب غير متوفرة بسبب رفض أذونات المتصفح مسبقاً", "notifications.permission_denied": "تنبيهات سطح المكتب غير متوفرة بسبب رفض أذونات المتصفح مسبقاً",
"notifications.permission_denied_alert": "لا يمكن تفعيل إشعارات سطح المكتب، لأن إذن المتصفح قد تم رفضه سابقاً", "notifications.permission_denied_alert": "لا يمكن تفعيل إشعارات سطح المكتب، لأن إذن المتصفح قد تم رفضه سابقاً",
"notifications.permission_required": "إشعارات سطح المكتب غير متوفرة لأنه لم يتم منح الإذن المطلوب.", "notifications.permission_required": "إشعارات سطح المكتب غير متوفرة لأنه لم يتم منح الإذن المطلوب.",
"notifications.policy.filter_new_accounts_title": "حسابات جديدة",
"notifications.policy.filter_not_followers_title": "أشخاص لا يتابعونك",
"notifications.policy.filter_not_following_hint": "حتى توافق عليهم يدويا",
"notifications.policy.filter_not_following_title": "أشخاص لا تتابعهم",
"notifications_permission_banner.enable": "تفعيل إشعارات سطح المكتب", "notifications_permission_banner.enable": "تفعيل إشعارات سطح المكتب",
"notifications_permission_banner.how_to_control": "لتلقي الإشعارات عندما لا يكون ماستدون مفتوح، قم بتفعيل إشعارات سطح المكتب، يمكنك التحكم بدقة في أنواع التفاعلات التي تولد إشعارات سطح المكتب من خلال زر الـ{icon} أعلاه بمجرد تفعيلها.", "notifications_permission_banner.how_to_control": "لتلقي الإشعارات عندما لا يكون ماستدون مفتوح، قم بتفعيل إشعارات سطح المكتب، يمكنك التحكم بدقة في أنواع التفاعلات التي تولد إشعارات سطح المكتب من خلال زر الـ{icon} أعلاه بمجرد تفعيلها.",
"notifications_permission_banner.title": "لا تفوت شيئاً أبداً", "notifications_permission_banner.title": "لا تفوت شيئاً أبداً",
@ -638,6 +683,7 @@
"status.direct": "إشارة خاصة لـ @{name}", "status.direct": "إشارة خاصة لـ @{name}",
"status.direct_indicator": "إشارة خاصة", "status.direct_indicator": "إشارة خاصة",
"status.edit": "تعديل", "status.edit": "تعديل",
"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.favourite": "فضّل",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Скарыстайцеся існуючай катэгорыяй або стварыце новую", "filter_modal.select_filter.subtitle": "Скарыстайцеся існуючай катэгорыяй або стварыце новую",
"filter_modal.select_filter.title": "Фільтраваць гэты допіс", "filter_modal.select_filter.title": "Фільтраваць гэты допіс",
"filter_modal.title.status": "Фільтраваць допіс", "filter_modal.title.status": "Фільтраваць допіс",
"filtered_notifications_banner.mentions": "{count, plural, one {згадванне} few {згадванні} many {згадванняў} other {згадвання}}",
"filtered_notifications_banner.pending_requests": "Апавяшчэнні ад {count, plural, =0 {# людзей якіх} one {# чалавека якіх} few {# чалавек якіх} many {# людзей якіх} other {# чалавека якіх}} вы магчыма ведаеце", "filtered_notifications_banner.pending_requests": "Апавяшчэнні ад {count, plural, =0 {# людзей якіх} one {# чалавека якіх} few {# чалавек якіх} many {# людзей якіх} other {# чалавека якіх}} вы магчыма ведаеце",
"filtered_notifications_banner.title": "Адфільтраваныя апавяшчэнні", "filtered_notifications_banner.title": "Адфільтраваныя апавяшчэнні",
"firehose.all": "Усе", "firehose.all": "Усе",
@ -471,6 +472,7 @@
"notification.own_poll": "Ваша апытанне скончылася", "notification.own_poll": "Ваша апытанне скончылася",
"notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася", "notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася",
"notification.reblog": "{name} пашырыў ваш допіс", "notification.reblog": "{name} пашырыў ваш допіс",
"notification.relationships_severance_event.learn_more": "Даведацца больш",
"notification.status": "Новы допіс ад {name}", "notification.status": "Новы допіс ад {name}",
"notification.update": "Допіс {name} адрэдагаваны", "notification.update": "Допіс {name} адрэдагаваны",
"notification_requests.accept": "Прыняць", "notification_requests.accept": "Прыняць",
@ -483,6 +485,8 @@
"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.favourite": "Упадабанае:",
"notifications.column_settings.filter_bar.advanced": "Паказаць усе катэгорыі",
"notifications.column_settings.filter_bar.category": "Панэль хуткай фільтрацыі",
"notifications.column_settings.follow": "Новыя падпісчыкі:", "notifications.column_settings.follow": "Новыя падпісчыкі:",
"notifications.column_settings.follow_request": "Новыя запыты на падпіску:", "notifications.column_settings.follow_request": "Новыя запыты на падпіску:",
"notifications.column_settings.mention": "Згадванні:", "notifications.column_settings.mention": "Згадванні:",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Изберете съществуваща категория или създайте нова", "filter_modal.select_filter.subtitle": "Изберете съществуваща категория или създайте нова",
"filter_modal.select_filter.title": "Филтриране на публ.", "filter_modal.select_filter.title": "Филтриране на публ.",
"filter_modal.title.status": "Филтриране на публ.", "filter_modal.title.status": "Филтриране на публ.",
"filtered_notifications_banner.mentions": "{count, plural, one {споменаване} other {споменавания}}",
"filtered_notifications_banner.pending_requests": "Известията от {count, plural, =0 {никого, когото може да познавате} one {едно лице, което може да познавате} other {# души, които може да познавате}}", "filtered_notifications_banner.pending_requests": "Известията от {count, plural, =0 {никого, когото може да познавате} one {едно лице, което може да познавате} other {# души, които може да познавате}}",
"filtered_notifications_banner.title": "Филтрирани известия", "filtered_notifications_banner.title": "Филтрирани известия",
"firehose.all": "Всичко", "firehose.all": "Всичко",
@ -471,7 +472,11 @@
"notification.own_poll": "Анкетата ви приключи", "notification.own_poll": "Анкетата ви приключи",
"notification.poll": "Анкета, в която гласувахте, приключи", "notification.poll": "Анкета, в която гласувахте, приключи",
"notification.reblog": "{name} подсили ваша публикация", "notification.reblog": "{name} подсили ваша публикация",
"notification.severed_relationships": "Връзката с {name} е прекъсната", "notification.relationships_severance_event": "Изгуби се връзката с {name}",
"notification.relationships_severance_event.account_suspension": "Администратор от {from} спря {target}, което значи че повече не може да получавате новости от тях или да взаимодействате с тях.",
"notification.relationships_severance_event.domain_block": "Администратор от {from} блокира {target}, вкючващо {followersCount} от последователите ви и {followingCount, plural, one {# акаунт, който} other {# акаунта, които}} следвате.",
"notification.relationships_severance_event.learn_more": "Научете повече",
"notification.relationships_severance_event.user_domain_block": "Блокирахте {target}, премахвайки {followersCount} от последователите си и {followingCount, plural, one {# акаунт, който} other {# акаунта, които}} следвате.",
"notification.status": "{name} току-що публикува", "notification.status": "{name} току-що публикува",
"notification.update": "{name} промени публикация", "notification.update": "{name} промени публикация",
"notification_requests.accept": "Приемам", "notification_requests.accept": "Приемам",
@ -484,6 +489,8 @@
"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.favourite": "Любими:",
"notifications.column_settings.filter_bar.advanced": "Показване на всички категории",
"notifications.column_settings.filter_bar.category": "Лента за бърз филтър",
"notifications.column_settings.follow": "Нови последователи:", "notifications.column_settings.follow": "Нови последователи:",
"notifications.column_settings.follow_request": "Нови заявки за последване:", "notifications.column_settings.follow_request": "Нови заявки за последване:",
"notifications.column_settings.mention": "Споменавания:", "notifications.column_settings.mention": "Споменавания:",
@ -588,12 +595,6 @@
"refresh": "Опресняване", "refresh": "Опресняване",
"regeneration_indicator.label": "Зареждане…", "regeneration_indicator.label": "Зареждане…",
"regeneration_indicator.sublabel": "Подготовка на началния ви инфоканал!", "regeneration_indicator.sublabel": "Подготовка на началния ви инфоканал!",
"relationship_severance_notification.purged_data": "прочистено от администраторите",
"relationship_severance_notification.relationships": "{count, plural, one {# връзка} other {# връзки}}",
"relationship_severance_notification.types.account_suspension": "Акаунтът е спрян",
"relationship_severance_notification.types.domain_block": "Домейнът е спрян",
"relationship_severance_notification.types.user_domain_block": "Блокирахте този домейн",
"relationship_severance_notification.view": "Преглед",
"relative_time.days": "{number} д.", "relative_time.days": "{number} д.",
"relative_time.full.days": "преди {number, plural, one {# ден} other {# дни}}", "relative_time.full.days": "преди {number, plural, one {# ден} other {# дни}}",
"relative_time.full.hours": "преди {number, plural, one {# час} other {# часа}}", "relative_time.full.hours": "преди {number, plural, one {# час} other {# часа}}",

View file

@ -256,6 +256,7 @@
"filter_modal.select_filter.subtitle": "Implijout ur rummad a zo anezhañ pe krouiñ unan nevez", "filter_modal.select_filter.subtitle": "Implijout ur rummad a zo anezhañ pe krouiñ unan nevez",
"filter_modal.select_filter.title": "Silañ an toud-mañ", "filter_modal.select_filter.title": "Silañ an toud-mañ",
"filter_modal.title.status": "Silañ un toud", "filter_modal.title.status": "Silañ un toud",
"filtered_notifications_banner.mentions": "{count, plural, one {meneg} two {veneg} few {meneg} other {a venegoù}}",
"firehose.all": "Pep tra", "firehose.all": "Pep tra",
"firehose.local": "Ar servijer-mañ", "firehose.local": "Ar servijer-mañ",
"firehose.remote": "Servijerioù all", "firehose.remote": "Servijerioù all",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Usa una categoria existent o crea'n una de nova", "filter_modal.select_filter.subtitle": "Usa una categoria existent o crea'n una de nova",
"filter_modal.select_filter.title": "Filtra aquest tut", "filter_modal.select_filter.title": "Filtra aquest tut",
"filter_modal.title.status": "Filtra un tut", "filter_modal.title.status": "Filtra un tut",
"filtered_notifications_banner.mentions": "{count, plural, one {menció} other {mencions}}",
"filtered_notifications_banner.pending_requests": "Notificacions {count, plural, =0 {de ningú} one {d'una persona} other {de # persones}} que potser coneixes", "filtered_notifications_banner.pending_requests": "Notificacions {count, plural, =0 {de ningú} one {d'una persona} other {de # persones}} que potser coneixes",
"filtered_notifications_banner.title": "Notificacions filtrades", "filtered_notifications_banner.title": "Notificacions filtrades",
"firehose.all": "Tots", "firehose.all": "Tots",
@ -307,6 +308,8 @@
"follow_requests.unlocked_explanation": "Tot i que el teu compte no està blocat, el personal de {domain} ha pensat que és possible que vulguis revisar manualment les sol·licituds de seguiment daquests comptes.", "follow_requests.unlocked_explanation": "Tot i que el teu compte no està blocat, el personal de {domain} ha pensat que és possible que vulguis revisar manualment les sol·licituds de seguiment daquests comptes.",
"follow_suggestions.curated_suggestion": "Tria de l'equip", "follow_suggestions.curated_suggestion": "Tria de l'equip",
"follow_suggestions.dismiss": "No ho tornis a mostrar", "follow_suggestions.dismiss": "No ho tornis a mostrar",
"follow_suggestions.featured_longer": "Triat personalment per l'equip de {domain}",
"follow_suggestions.friends_of_friends_longer": "Popular entre la gent que segueixes",
"follow_suggestions.hints.featured": "L'equip de {domain} ha seleccionat aquest perfil.", "follow_suggestions.hints.featured": "L'equip de {domain} ha seleccionat aquest perfil.",
"follow_suggestions.hints.friends_of_friends": "Aquest perfil és popular entre la gent que seguiu.", "follow_suggestions.hints.friends_of_friends": "Aquest perfil és popular entre la gent que seguiu.",
"follow_suggestions.hints.most_followed": "Aquest perfil és un dels més seguits a {domain}.", "follow_suggestions.hints.most_followed": "Aquest perfil és un dels més seguits a {domain}.",
@ -314,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "Aquest perfil és similar a d'altres que heu seguit recentment.", "follow_suggestions.hints.similar_to_recently_followed": "Aquest perfil és similar a d'altres que heu seguit recentment.",
"follow_suggestions.personalized_suggestion": "Suggeriment personalitzat", "follow_suggestions.personalized_suggestion": "Suggeriment personalitzat",
"follow_suggestions.popular_suggestion": "Suggeriment popular", "follow_suggestions.popular_suggestion": "Suggeriment popular",
"follow_suggestions.popular_suggestion_longer": "Popular a {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Semblant a perfils que has seguit fa poc",
"follow_suggestions.view_all": "Mostra-ho tot", "follow_suggestions.view_all": "Mostra-ho tot",
"follow_suggestions.who_to_follow": "A qui seguir", "follow_suggestions.who_to_follow": "A qui seguir",
"followed_tags": "Etiquetes seguides", "followed_tags": "Etiquetes seguides",
@ -471,7 +476,11 @@
"notification.own_poll": "La teva enquesta ha finalitzat", "notification.own_poll": "La teva enquesta ha finalitzat",
"notification.poll": "Ha finalitzat una enquesta en què has votat", "notification.poll": "Ha finalitzat una enquesta en què has votat",
"notification.reblog": "{name} t'ha impulsat", "notification.reblog": "{name} t'ha impulsat",
"notification.severed_relationships": "S'han eliminat les relacions amb {name}", "notification.relationships_severance_event": "S'han perdut les connexions amb {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspès {target}; això vol dir que ja no en podreu rebre actualitzacions o interactuar-hi.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} ha blocat {target}, incloent-hi {followersCount} dels vostres seguidors i {followingCount, plural, one {# compte} other {# comptes}} que seguiu.",
"notification.relationships_severance_event.learn_more": "Per a saber-ne més",
"notification.relationships_severance_event.user_domain_block": "Heu blocat {target}, eliminant {followersCount} dels vostres seguidors i {followingCount, plural, one {# compte} other {# comptes}} que seguiu.",
"notification.status": "{name} acaba de publicar", "notification.status": "{name} acaba de publicar",
"notification.update": "{name} ha editat un tut", "notification.update": "{name} ha editat un tut",
"notification_requests.accept": "Accepta", "notification_requests.accept": "Accepta",
@ -484,6 +493,8 @@
"notifications.column_settings.admin.sign_up": "Registres nous:", "notifications.column_settings.admin.sign_up": "Registres nous:",
"notifications.column_settings.alert": "Notificacions d'escriptori", "notifications.column_settings.alert": "Notificacions d'escriptori",
"notifications.column_settings.favourite": "Favorits:", "notifications.column_settings.favourite": "Favorits:",
"notifications.column_settings.filter_bar.advanced": "Mostra totes les categories",
"notifications.column_settings.filter_bar.category": "Barra ràpida de filtres",
"notifications.column_settings.follow": "Nous seguidors:", "notifications.column_settings.follow": "Nous seguidors:",
"notifications.column_settings.follow_request": "Noves sol·licituds de seguiment:", "notifications.column_settings.follow_request": "Noves sol·licituds de seguiment:",
"notifications.column_settings.mention": "Mencions:", "notifications.column_settings.mention": "Mencions:",
@ -588,12 +599,6 @@
"refresh": "Actualitza", "refresh": "Actualitza",
"regeneration_indicator.label": "Es carrega…", "regeneration_indicator.label": "Es carrega…",
"regeneration_indicator.sublabel": "Es prepara la teva línia de temps d'Inici!", "regeneration_indicator.sublabel": "Es prepara la teva línia de temps d'Inici!",
"relationship_severance_notification.purged_data": "purgat pels administradors",
"relationship_severance_notification.relationships": "{count, plural, one {# relació} other {# relacions}}",
"relationship_severance_notification.types.account_suspension": "S'ha suspès el compte",
"relationship_severance_notification.types.domain_block": "S'ha suspès el domini",
"relationship_severance_notification.types.user_domain_block": "Heu blocat aquest domini",
"relationship_severance_notification.view": "Visualitza",
"relative_time.days": "{number}d", "relative_time.days": "{number}d",
"relative_time.full.days": "fa {number, plural, one {# dia} other {# dies}}", "relative_time.full.days": "fa {number, plural, one {# dia} other {# dies}}",
"relative_time.full.hours": "fa {number, plural, one {# hora} other {# hores}}", "relative_time.full.hours": "fa {number, plural, one {# hora} other {# hores}}",
@ -704,7 +709,7 @@
"status.edited_x_times": "Editat {count, plural, one {{count} vegada} other {{count} vegades}}", "status.edited_x_times": "Editat {count, plural, one {{count} vegada} other {{count} vegades}}",
"status.embed": "Incrusta", "status.embed": "Incrusta",
"status.favourite": "Favorit", "status.favourite": "Favorit",
"status.favourites": "{count, plural, one {# favorit} other {# favorits}}", "status.favourites": "{count, plural, one {favorit} other {favorits}}",
"status.filter": "Filtra aquest tut", "status.filter": "Filtra aquest tut",
"status.filtered": "Filtrada", "status.filtered": "Filtrada",
"status.hide": "Amaga el tut", "status.hide": "Amaga el tut",
@ -725,7 +730,7 @@
"status.reblog": "Impulsa", "status.reblog": "Impulsa",
"status.reblog_private": "Impulsa amb la visibilitat original", "status.reblog_private": "Impulsa amb la visibilitat original",
"status.reblogged_by": "impulsat per {name}", "status.reblogged_by": "impulsat per {name}",
"status.reblogs": "{count, plural, one {# impuls} other {# impulsos}}", "status.reblogs": "{count, plural, one {impuls} other {impulsos}}",
"status.reblogs.empty": "Encara no ha impulsat ningú aquest tut. Quan algú ho faci, apareixerà aquí.", "status.reblogs.empty": "Encara no ha impulsat ningú aquest tut. Quan algú ho faci, apareixerà aquí.",
"status.redraft": "Esborra i reescriu", "status.redraft": "Esborra i reescriu",
"status.remove_bookmark": "Elimina el marcador", "status.remove_bookmark": "Elimina el marcador",

View file

@ -89,6 +89,14 @@
"announcement.announcement": "Oznámení", "announcement.announcement": "Oznámení",
"attachments_list.unprocessed": "(nezpracováno)", "attachments_list.unprocessed": "(nezpracováno)",
"audio.hide": "Skrýt zvuk", "audio.hide": "Skrýt zvuk",
"block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou být stále viditelné pro nepřihlášené uživatele.",
"block_modal.show_less": "Zobrazit méně",
"block_modal.show_more": "Zobrazit více",
"block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.",
"block_modal.they_cant_see_posts": "Nemůže vidět vaše příspěvky a vy neuvidíte jeho.",
"block_modal.they_will_know": "Může vidět, že je zablokovaný.",
"block_modal.title": "Zablokovat uživatele?",
"block_modal.you_wont_see_mentions": "Neuvidíte příspěvky, které ho zmiňují.",
"boost_modal.combo": "Příště můžete pro přeskočení stisknout {combo}", "boost_modal.combo": "Příště můžete pro přeskočení stisknout {combo}",
"bundle_column_error.copy_stacktrace": "Zkopírovat zprávu o chybě", "bundle_column_error.copy_stacktrace": "Zkopírovat zprávu o chybě",
"bundle_column_error.error.body": "Požadovanou stránku nelze vykreslit. Může to být způsobeno chybou v našem kódu nebo problémem s kompatibilitou prohlížeče.", "bundle_column_error.error.body": "Požadovanou stránku nelze vykreslit. Může to být způsobeno chybou v našem kódu nebo problémem s kompatibilitou prohlížeče.",
@ -169,6 +177,7 @@
"confirmations.delete_list.message": "Opravdu chcete tento seznam navždy smazat?", "confirmations.delete_list.message": "Opravdu chcete tento seznam navždy smazat?",
"confirmations.discard_edit_media.confirm": "Zahodit", "confirmations.discard_edit_media.confirm": "Zahodit",
"confirmations.discard_edit_media.message": "Máte neuložené změny popisku médií nebo náhledu, chcete je přesto zahodit?", "confirmations.discard_edit_media.message": "Máte neuložené změny popisku médií nebo náhledu, chcete je přesto zahodit?",
"confirmations.domain_block.confirm": "Blokovat server",
"confirmations.domain_block.message": "Opravdu chcete blokovat celou doménu {domain}? Ve většině případů stačí blokovat nebo skrýt pár konkrétních uživatelů, což také doporučujeme. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.", "confirmations.domain_block.message": "Opravdu chcete blokovat celou doménu {domain}? Ve většině případů stačí blokovat nebo skrýt pár konkrétních uživatelů, což také doporučujeme. Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.",
"confirmations.edit.confirm": "Upravit", "confirmations.edit.confirm": "Upravit",
"confirmations.edit.message": "Editovat teď znamená přepsání zprávy, kterou právě tvoříte. Opravdu chcete pokračovat?", "confirmations.edit.message": "Editovat teď znamená přepsání zprávy, kterou právě tvoříte. Opravdu chcete pokračovat?",
@ -200,6 +209,27 @@
"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_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}.", "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}.",
"domain_block_modal.block": "Blokovat server",
"domain_block_modal.block_account_instead": "Raději blokovat @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Lidé z tohoto serveru mohou interagovat s vašimi starými příspěvky.",
"domain_block_modal.they_cant_follow": "Nikdo z tohoto serveru vás nemůže sledovat.",
"domain_block_modal.they_wont_know": "Nebude vědět, že je zablokován.",
"domain_block_modal.title": "Blokovat doménu?",
"domain_block_modal.you_will_lose_followers": "Všichni vaši sledující z tohoto serveru budou odstraněni.",
"domain_block_modal.you_wont_see_posts": "Neuvidíte příspěvky ani upozornění od uživatelů z tohoto serveru.",
"domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i s dalšími sociálními aplikacemi.",
"domain_pill.activitypub_like_language": "ActivityPub je jako jazyk, kterým Mastodon mluví s jinými sociálními sítěmi.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Handle:",
"domain_pill.their_server": "Digitální domov, kde žijí všechny příspěvky.",
"domain_pill.their_username": "Jedinečný identikátor na serveru. Je možné najít uživatele se stejným uživatelským jménem na různých serverech.",
"domain_pill.username": "Uživatelské jméno",
"domain_pill.whats_in_a_handle": "Co obsahuje handle?",
"domain_pill.who_they_are": "Protože handle říkají kdo je kdo a také kde, je možné interagovat s lidmi napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
"domain_pill.who_you_are": "Protože handle říká kdo jsi a kde jsi, mohou s tebou lidé komunikovat napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
"domain_pill.your_handle": "Tvůj handle:",
"domain_pill.your_server": "Tvůj digitální domov, kde žijí všechny tvé příspěvky. Nelíbí se ti? Kdykoliv se přesuň na jiný server a vezmi si sebou i své sledující.",
"domain_pill.your_username": "Tvůj jedinečný identifikátor na tomto serveru. Je možné najít uživatele se stejným uživatelským jménem na jiných serverech.",
"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",
@ -236,6 +266,7 @@
"empty_column.list": "V tomto seznamu zatím nic není. Až nějaký člen z tohoto seznamu zveřejní nový příspěvek, objeví se zde.", "empty_column.list": "V tomto seznamu zatím nic není. Až nějaký člen z tohoto seznamu zveřejní nový příspěvek, objeví se zde.",
"empty_column.lists": "Zatím nemáte žádné seznamy. Až nějaký vytvoříte, zobrazí se zde.", "empty_column.lists": "Zatím nemáte žádné seznamy. Až nějaký vytvoříte, zobrazí se zde.",
"empty_column.mutes": "Zatím jste neskryli žádného uživatele.", "empty_column.mutes": "Zatím jste neskryli žádného uživatele.",
"empty_column.notification_requests": "Vyčištěno! Nic tu není. Jakmile obdržíš nové notifikace, objeví se zde podle tvého nastavení.",
"empty_column.notifications": "Zatím nemáte žádná oznámení. Až s vámi někdo bude interagovat, uvidíte to zde.", "empty_column.notifications": "Zatím nemáte žádná oznámení. Až s vámi někdo bude interagovat, uvidíte to zde.",
"empty_column.public": "Tady nic není! Napište něco veřejně, nebo začněte ručně sledovat uživatele z jiných serverů, aby tu něco přibylo", "empty_column.public": "Tady nic není! Napište něco veřejně, nebo začněte ručně sledovat uživatele z jiných serverů, aby tu něco přibylo",
"error.unexpected_crash.explanation": "Kvůli chybě v našem kódu nebo problému s kompatibilitou prohlížeče nemohla být tato stránka správně zobrazena.", "error.unexpected_crash.explanation": "Kvůli chybě v našem kódu nebo problému s kompatibilitou prohlížeče nemohla být tato stránka správně zobrazena.",
@ -266,6 +297,9 @@
"filter_modal.select_filter.subtitle": "Použít existující kategorii nebo vytvořit novou kategorii", "filter_modal.select_filter.subtitle": "Použít existující kategorii nebo vytvořit novou kategorii",
"filter_modal.select_filter.title": "Filtrovat tento příspěvek", "filter_modal.select_filter.title": "Filtrovat tento příspěvek",
"filter_modal.title.status": "Filtrovat příspěvek", "filter_modal.title.status": "Filtrovat příspěvek",
"filtered_notifications_banner.mentions": "{count, plural, one {zmínka} few {zmínky} many {zmínek} other {zmínek}}",
"filtered_notifications_banner.pending_requests": "Oznámení od {count, plural, =0 {nikoho} one {jednoho člověka, kterého znáte} few {# lidí, které znáte} many {# lidí, které znáte} other {# lidí, které znáte}}",
"filtered_notifications_banner.title": "Filtrovaná oznámení",
"firehose.all": "Vše", "firehose.all": "Vše",
"firehose.local": "Tento server", "firehose.local": "Tento server",
"firehose.remote": "Ostatní servery", "firehose.remote": "Ostatní servery",
@ -394,6 +428,15 @@
"loading_indicator.label": "Načítání…", "loading_indicator.label": "Načítání…",
"media_gallery.toggle_visible": "{number, plural, one {Skrýt obrázek} few {Skrýt obrázky} many {Skrýt obrázky} other {Skrýt obrázky}}", "media_gallery.toggle_visible": "{number, plural, one {Skrýt obrázek} few {Skrýt obrázky} many {Skrýt obrázky} other {Skrýt obrázky}}",
"moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.", "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
"mute_modal.hide_from_notifications": "Skrýt z notifikací",
"mute_modal.hide_options": "Skrýt možnosti",
"mute_modal.indefinite": "Dokud je neodkryju",
"mute_modal.show_options": "Zobrazit možnosti",
"mute_modal.they_can_mention_and_follow": "Mohou vás zmínit a sledovat, ale neuvidíte je.",
"mute_modal.they_wont_know": "Nebudou vědět, že byli skryti.",
"mute_modal.title": "Ztlumit uživatele?",
"mute_modal.you_wont_see_mentions": "Neuvidíte příspěvky, které je zmiňují.",
"mute_modal.you_wont_see_posts": "Stále budou moci vidět vaše příspěvky, ale vy jejich neuvidíte.",
"navigation_bar.about": "O aplikaci", "navigation_bar.about": "O aplikaci",
"navigation_bar.advanced_interface": "Otevřít pokročilé webové rozhraní", "navigation_bar.advanced_interface": "Otevřít pokročilé webové rozhraní",
"navigation_bar.blocks": "Blokovaní uživatelé", "navigation_bar.blocks": "Blokovaní uživatelé",
@ -429,14 +472,25 @@
"notification.own_poll": "Vaše anketa skončila", "notification.own_poll": "Vaše anketa skončila",
"notification.poll": "Anketa, ve které jste hlasovali, skončila", "notification.poll": "Anketa, ve které jste hlasovali, skončila",
"notification.reblog": "Uživatel {name} boostnul váš příspěvek", "notification.reblog": "Uživatel {name} boostnul váš příspěvek",
"notification.relationships_severance_event": "Kontakt ztracen s {name}",
"notification.relationships_severance_event.account_suspension": "Administrátor z {from} pozastavil {target}, což znamená, že již od nich nemůžete přijímat aktualizace nebo s nimi interagovat.",
"notification.relationships_severance_event.domain_block": "Administrátor z {from} pozastavil {target}, včetně {followersCount} z vašich sledujících a {followingCount, plural, one {# účet, který sledujete} few {# účty, které sledujete} many {# účtů, které sledujete} other {# účtů, které sledujete}}.",
"notification.relationships_severance_event.learn_more": "Zjistit více",
"notification.relationships_severance_event.user_domain_block": "Zablokovali jste {target}, čímž jste odebrali {followersCount} z vašich sledujících a {followingCount, plural, one {# účet, který sledujete} few {# účty, které sledujete} many {# účtů, které sledujete} other {# účtů, které sledujete}}.",
"notification.status": "Uživatel {name} právě přidal příspěvek", "notification.status": "Uživatel {name} právě přidal příspěvek",
"notification.update": "Uživatel {name} upravil příspěvek", "notification.update": "Uživatel {name} upravil příspěvek",
"notification_requests.accept": "Přijmout",
"notification_requests.dismiss": "Zamítnout",
"notification_requests.notifications_from": "Oznámení od {name}",
"notification_requests.title": "Vyfiltrovaná oznámení",
"notifications.clear": "Vyčistit oznámení", "notifications.clear": "Vyčistit oznámení",
"notifications.clear_confirmation": "Opravdu chcete trvale smazat všechna vaše oznámení?", "notifications.clear_confirmation": "Opravdu chcete trvale smazat všechna vaše oznámení?",
"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.favourite": "Oblíbené:",
"notifications.column_settings.filter_bar.advanced": "Zobrazit všechny kategorie",
"notifications.column_settings.filter_bar.category": "Panel rychlého filtrování",
"notifications.column_settings.follow": "Noví sledující:", "notifications.column_settings.follow": "Noví sledující:",
"notifications.column_settings.follow_request": "Nové žádosti o sledování:", "notifications.column_settings.follow_request": "Nové žádosti o sledování:",
"notifications.column_settings.mention": "Zmínky:", "notifications.column_settings.mention": "Zmínky:",
@ -462,6 +516,15 @@
"notifications.permission_denied": "Oznámení na ploše nejsou k dispozici, protože byla zamítnuta žádost o oprávnění je zobrazovat", "notifications.permission_denied": "Oznámení na ploše nejsou k dispozici, protože byla zamítnuta žádost o oprávnění je zobrazovat",
"notifications.permission_denied_alert": "Oznámení na ploše není možné zapnout, protože oprávnění bylo v minulosti zamítnuto", "notifications.permission_denied_alert": "Oznámení na ploše není možné zapnout, protože oprávnění bylo v minulosti zamítnuto",
"notifications.permission_required": "Oznámení na ploše nejsou k dispozici, protože nebylo uděleno potřebné oprávnění.", "notifications.permission_required": "Oznámení na ploše nejsou k dispozici, protože nebylo uděleno potřebné oprávnění.",
"notifications.policy.filter_new_accounts.hint": "Vytvořeno během {days, plural, one {včerejška} few {posledních # dnů} many {posledních # dní} other {posledních # dní}}",
"notifications.policy.filter_new_accounts_title": "Nové účty",
"notifications.policy.filter_not_followers_hint": "Včetně lidí, kteří vás sledovali méně než {days, plural, one {jeden den} few {# dny} many {# dní} other {# dní}}",
"notifications.policy.filter_not_followers_title": "Lidé, kteří vás nesledují",
"notifications.policy.filter_not_following_hint": "Dokud je ručně neschválíte",
"notifications.policy.filter_not_following_title": "Lidé, které nesledujete",
"notifications.policy.filter_private_mentions_hint": "Vyfiltrováno, pokud to není odpověď na vaši zmínku nebo pokud sledujete odesílatele",
"notifications.policy.filter_private_mentions_title": "Nevyžádané soukromé zmínky",
"notifications.policy.title": "Vyfiltrovat oznámení od…",
"notifications_permission_banner.enable": "Povolit oznámení na ploše", "notifications_permission_banner.enable": "Povolit oznámení na ploše",
"notifications_permission_banner.how_to_control": "Chcete-li dostávat oznámení, i když nemáte Mastodon otevřený, povolte oznámení na ploše. Můžete si zvolit, o kterých druzích interakcí chcete být oznámením na ploše informování pod tlačítkem {icon} výše.", "notifications_permission_banner.how_to_control": "Chcete-li dostávat oznámení, i když nemáte Mastodon otevřený, povolte oznámení na ploše. Můžete si zvolit, o kterých druzích interakcí chcete být oznámením na ploše informování pod tlačítkem {icon} výše.",
"notifications_permission_banner.title": "Nenechte si nic uniknout", "notifications_permission_banner.title": "Nenechte si nic uniknout",
@ -638,9 +701,11 @@
"status.direct": "Soukromě zmínit @{name}", "status.direct": "Soukromě zmínit @{name}",
"status.direct_indicator": "Soukromá zmínka", "status.direct_indicator": "Soukromá zmínka",
"status.edit": "Upravit", "status.edit": "Upravit",
"status.edited": "Naposledy 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.favourite": "Oblíbit",
"status.favourites": "{count, plural, one {oblíbený} few {oblíbené} many {oblíbených} other {oblíbených}}",
"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",
@ -661,6 +726,7 @@
"status.reblog": "Boostnout", "status.reblog": "Boostnout",
"status.reblog_private": "Boostnout s původní viditelností", "status.reblog_private": "Boostnout s původní viditelností",
"status.reblogged_by": "Uživatel {name} boostnul", "status.reblogged_by": "Uživatel {name} boostnul",
"status.reblogs": "{count, plural, one {boost} few {boosty} many {boostů} other {boostů}}",
"status.reblogs.empty": "Tento příspěvek ještě nikdo neboostnul. Pokud to někdo udělá, zobrazí se zde.", "status.reblogs.empty": "Tento příspěvek ještě nikdo neboostnul. Pokud to někdo udělá, zobrazí se zde.",
"status.redraft": "Smazat a přepsat", "status.redraft": "Smazat a přepsat",
"status.remove_bookmark": "Odstranit ze záložek", "status.remove_bookmark": "Odstranit ze záložek",

View file

@ -220,7 +220,7 @@
"domain_pill.activitypub_lets_connect": "Det muliggør at komme i forbindelse og interagere med folk ikke kun på Mastodon, men også på tværs af forskellige sociale apps.", "domain_pill.activitypub_lets_connect": "Det muliggør at komme i forbindelse og interagere med folk ikke kun på Mastodon, men også på tværs af forskellige sociale apps.",
"domain_pill.activitypub_like_language": "ActivityPub er \"sproget\", Mastodon taler med andre sociale netværk.", "domain_pill.activitypub_like_language": "ActivityPub er \"sproget\", Mastodon taler med andre sociale netværk.",
"domain_pill.server": "Server", "domain_pill.server": "Server",
"domain_pill.their_handle": "Deres handle:", "domain_pill.their_handle": "Vedkommendes handle:",
"domain_pill.username": "Brugernavn", "domain_pill.username": "Brugernavn",
"domain_pill.whats_in_a_handle": "Hvad er der i et handle (@brugernavn)?", "domain_pill.whats_in_a_handle": "Hvad er der i et handle (@brugernavn)?",
"domain_pill.who_they_are": "Da et handle fortæller, hvem nogen er, og hvor de er, kan man interagere med folk på tværs af det sociale net af <button>ActivityPub-drevne platforme</button>.", "domain_pill.who_they_are": "Da et handle fortæller, hvem nogen er, og hvor de er, kan man interagere med folk på tværs af det sociale net af <button>ActivityPub-drevne platforme</button>.",
@ -295,6 +295,7 @@
"filter_modal.select_filter.subtitle": "Vælg en eksisterende kategori eller opret en ny", "filter_modal.select_filter.subtitle": "Vælg en eksisterende kategori eller opret en ny",
"filter_modal.select_filter.title": "Filtrér dette indlæg", "filter_modal.select_filter.title": "Filtrér dette indlæg",
"filter_modal.title.status": "Filtrér et indlæg", "filter_modal.title.status": "Filtrér et indlæg",
"filtered_notifications_banner.mentions": "{count, plural, one {omtale} other {omtaler}}",
"filtered_notifications_banner.pending_requests": "Notifikationer fra {count, plural, =0 {ingen} one {én person} other {# personer}} du måske kender", "filtered_notifications_banner.pending_requests": "Notifikationer fra {count, plural, =0 {ingen} one {én person} other {# personer}} du måske kender",
"filtered_notifications_banner.title": "Filtrerede notifikationer", "filtered_notifications_banner.title": "Filtrerede notifikationer",
"firehose.all": "Alle", "firehose.all": "Alle",
@ -305,6 +306,8 @@
"follow_requests.unlocked_explanation": "Selvom din konto ikke er låst, synes {domain}-personalet, du måske bør gennemgå disse anmodninger manuelt.", "follow_requests.unlocked_explanation": "Selvom din konto ikke er låst, synes {domain}-personalet, du måske bør gennemgå disse anmodninger manuelt.",
"follow_suggestions.curated_suggestion": "Personaleudvalgt", "follow_suggestions.curated_suggestion": "Personaleudvalgt",
"follow_suggestions.dismiss": "Vis ikke igen", "follow_suggestions.dismiss": "Vis ikke igen",
"follow_suggestions.featured_longer": "Håndplukket af {domain}-teamet",
"follow_suggestions.friends_of_friends_longer": "Populært blandt personer, som følges",
"follow_suggestions.hints.featured": "Denne profil er håndplukket af {domain}-teamet.", "follow_suggestions.hints.featured": "Denne profil er håndplukket af {domain}-teamet.",
"follow_suggestions.hints.friends_of_friends": "Denne profil er populær blandt de personer, som følges.", "follow_suggestions.hints.friends_of_friends": "Denne profil er populær blandt de personer, som følges.",
"follow_suggestions.hints.most_followed": "Denne profil er en af de mest fulgte på {domain}.", "follow_suggestions.hints.most_followed": "Denne profil er en af de mest fulgte på {domain}.",
@ -312,6 +315,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "Denne profil svarer til de profiler, som senest er blevet fulgt.", "follow_suggestions.hints.similar_to_recently_followed": "Denne profil svarer til de profiler, som senest er blevet fulgt.",
"follow_suggestions.personalized_suggestion": "Personligt forslag", "follow_suggestions.personalized_suggestion": "Personligt forslag",
"follow_suggestions.popular_suggestion": "Populært forslag", "follow_suggestions.popular_suggestion": "Populært forslag",
"follow_suggestions.popular_suggestion_longer": "Populært på {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Svarende til profiler, som for nylig er fulgt",
"follow_suggestions.view_all": "Vis alle", "follow_suggestions.view_all": "Vis alle",
"follow_suggestions.who_to_follow": "Hvem, som skal følges", "follow_suggestions.who_to_follow": "Hvem, som skal følges",
"followed_tags": "Hashtag, som følges", "followed_tags": "Hashtag, som følges",
@ -466,9 +471,23 @@
"notification.follow": "{name} begyndte at følge dig", "notification.follow": "{name} begyndte at følge dig",
"notification.follow_request": "{name} har anmodet om at følge dig", "notification.follow_request": "{name} har anmodet om at følge dig",
"notification.mention": "{name} nævnte dig", "notification.mention": "{name} nævnte dig",
"notification.moderation-warning.learn_more": "Læs mere",
"notification.moderation_warning": "Du er tildelt en moderationsadvarsel",
"notification.moderation_warning.action_delete_statuses": "Nogle af dine indlæg er blevet fjernet.",
"notification.moderation_warning.action_disable": "Din konto er blevet deaktiveret.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Nogle af dine indlæg er blevet markeret som sensitive.",
"notification.moderation_warning.action_none": "Din konto er tildelt en moderationsadvarsel.",
"notification.moderation_warning.action_sensitive": "Dine indlæg markeres fra nu af som sensitive.",
"notification.moderation_warning.action_silence": "Din konto er blevet begrænset.",
"notification.moderation_warning.action_suspend": "Din konto er suspenderet.",
"notification.own_poll": "Din afstemning er afsluttet", "notification.own_poll": "Din afstemning er afsluttet",
"notification.poll": "En afstemning, hvori du stemte, er slut", "notification.poll": "En afstemning, hvori du stemte, er slut",
"notification.reblog": "{name} boostede dit indlæg", "notification.reblog": "{name} boostede dit indlæg",
"notification.relationships_severance_event": "Mistede forbindelser med {name}",
"notification.relationships_severance_event.account_suspension": "En admin fra {from} har suspenderet {target}, hvofor opdateringer herfra eller interaktion hermed ikke længer er mulig.",
"notification.relationships_severance_event.domain_block": "En admin fra {from} har blokeret {target}, herunder {followersCount} tilhængere og {followingCount, plural, one {# konto, der} other {# konti, som}} følges.",
"notification.relationships_severance_event.learn_more": "Læs mere",
"notification.relationships_severance_event.user_domain_block": "{target} er blevet blokeret, og {followersCount} tilhængere samt {followingCount, plural, one {# konto, der} other {# konti, som}} følges, er hermed fjernet.",
"notification.status": "{name} har netop postet", "notification.status": "{name} har netop postet",
"notification.update": "{name} redigerede et indlæg", "notification.update": "{name} redigerede et indlæg",
"notification_requests.accept": "Acceptér", "notification_requests.accept": "Acceptér",
@ -481,6 +500,8 @@
"notifications.column_settings.admin.sign_up": "Nye tilmeldinger:", "notifications.column_settings.admin.sign_up": "Nye tilmeldinger:",
"notifications.column_settings.alert": "Computernotifikationer", "notifications.column_settings.alert": "Computernotifikationer",
"notifications.column_settings.favourite": "Favoritter:", "notifications.column_settings.favourite": "Favoritter:",
"notifications.column_settings.filter_bar.advanced": "Vis alle kategorier",
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
"notifications.column_settings.follow": "Nye følgere:", "notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.follow_request": "Nye følgeanmodninger:", "notifications.column_settings.follow_request": "Nye følgeanmodninger:",
"notifications.column_settings.mention": "Omtaler:", "notifications.column_settings.mention": "Omtaler:",
@ -585,12 +606,6 @@
"refresh": "Genindlæs", "refresh": "Genindlæs",
"regeneration_indicator.label": "Indlæser…", "regeneration_indicator.label": "Indlæser…",
"regeneration_indicator.sublabel": "Din hjemmetidslinje klargøres!", "regeneration_indicator.sublabel": "Din hjemmetidslinje klargøres!",
"relationship_severance_notification.purged_data": "renset af administratorer",
"relationship_severance_notification.relationships": "{count, plural, one {# forhold} other {# forhold}}",
"relationship_severance_notification.types.account_suspension": "Konto er blevet suspenderet",
"relationship_severance_notification.types.domain_block": "Domæne er blevet suspenderet",
"relationship_severance_notification.types.user_domain_block": "Dette domæne blev blokeret",
"relationship_severance_notification.view": "Vis",
"relative_time.days": "{number}d", "relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# dag} other {# dage}} siden", "relative_time.full.days": "{number, plural, one {# dag} other {# dage}} siden",
"relative_time.full.hours": "{number, plural, one {# time} other {# timer}} siden", "relative_time.full.hours": "{number, plural, one {# time} other {# timer}} siden",

View file

@ -85,7 +85,7 @@
"alert.rate_limited.message": "Bitte versuche es nach {retry_time, time, medium} erneut.", "alert.rate_limited.message": "Bitte versuche es nach {retry_time, time, medium} erneut.",
"alert.rate_limited.title": "Anfragelimit überschritten", "alert.rate_limited.title": "Anfragelimit überschritten",
"alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.", "alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
"alert.unexpected.title": "Ups!", "alert.unexpected.title": "Oha!",
"announcement.announcement": "Ankündigung", "announcement.announcement": "Ankündigung",
"attachments_list.unprocessed": "(ausstehend)", "attachments_list.unprocessed": "(ausstehend)",
"audio.hide": "Audio ausblenden", "audio.hide": "Audio ausblenden",
@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Einem vorhandenen Filter hinzufügen oder einen neuen erstellen", "filter_modal.select_filter.subtitle": "Einem vorhandenen Filter hinzufügen oder einen neuen erstellen",
"filter_modal.select_filter.title": "Diesen Beitrag filtern", "filter_modal.select_filter.title": "Diesen Beitrag filtern",
"filter_modal.title.status": "Beitrag per Filter ausblenden", "filter_modal.title.status": "Beitrag per Filter ausblenden",
"filtered_notifications_banner.mentions": "{count, plural, one {Erwähnung} other {Erwähnungen}}",
"filtered_notifications_banner.pending_requests": "Benachrichtigungen von {count, plural, =0 {keinem Profil, das du möglicherweise kennst} one {einem Profil, das du möglicherweise kennst} other {# Profilen, die du möglicherweise kennst}}", "filtered_notifications_banner.pending_requests": "Benachrichtigungen von {count, plural, =0 {keinem Profil, das du möglicherweise kennst} one {einem Profil, das du möglicherweise kennst} other {# Profilen, die du möglicherweise kennst}}",
"filtered_notifications_banner.title": "Gefilterte Benachrichtigungen", "filtered_notifications_banner.title": "Gefilterte Benachrichtigungen",
"firehose.all": "Alles", "firehose.all": "Alles",
@ -307,6 +308,8 @@
"follow_requests.unlocked_explanation": "Auch wenn dein Konto öffentlich bzw. nicht geschützt ist, haben die Moderator*innen von {domain} gedacht, dass du diesen Follower lieber manuell bestätigen solltest.", "follow_requests.unlocked_explanation": "Auch wenn dein Konto öffentlich bzw. nicht geschützt ist, haben die Moderator*innen von {domain} gedacht, dass du diesen Follower lieber manuell bestätigen solltest.",
"follow_suggestions.curated_suggestion": "Vom Server-Team empfohlen", "follow_suggestions.curated_suggestion": "Vom Server-Team empfohlen",
"follow_suggestions.dismiss": "Nicht mehr anzeigen", "follow_suggestions.dismiss": "Nicht mehr anzeigen",
"follow_suggestions.featured_longer": "Vom {domain}-Team ausgewählt",
"follow_suggestions.friends_of_friends_longer": "Beliebt bei Leuten, denen du folgst",
"follow_suggestions.hints.featured": "Dieses Profil wurde vom {domain}-Team ausgewählt.", "follow_suggestions.hints.featured": "Dieses Profil wurde vom {domain}-Team ausgewählt.",
"follow_suggestions.hints.friends_of_friends": "Dieses Profil ist bei deinen Followern beliebt.", "follow_suggestions.hints.friends_of_friends": "Dieses Profil ist bei deinen Followern beliebt.",
"follow_suggestions.hints.most_followed": "Dieses Profil ist eines der am meisten gefolgten auf {domain}.", "follow_suggestions.hints.most_followed": "Dieses Profil ist eines der am meisten gefolgten auf {domain}.",
@ -314,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "Dieses Profil ähnelt den Profilen, denen du in letzter Zeit gefolgt hast.", "follow_suggestions.hints.similar_to_recently_followed": "Dieses Profil ähnelt den Profilen, denen du in letzter Zeit gefolgt hast.",
"follow_suggestions.personalized_suggestion": "Persönliche Empfehlung", "follow_suggestions.personalized_suggestion": "Persönliche Empfehlung",
"follow_suggestions.popular_suggestion": "Beliebte Empfehlung", "follow_suggestions.popular_suggestion": "Beliebte Empfehlung",
"follow_suggestions.popular_suggestion_longer": "Beliebt auf {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Ähnlich zu Profilen, denen du seit kurzem folgst",
"follow_suggestions.view_all": "Alle anzeigen", "follow_suggestions.view_all": "Alle anzeigen",
"follow_suggestions.who_to_follow": "Empfohlene Profile", "follow_suggestions.who_to_follow": "Empfohlene Profile",
"followed_tags": "Gefolgte Hashtags", "followed_tags": "Gefolgte Hashtags",
@ -468,10 +473,23 @@
"notification.follow": "{name} folgt dir", "notification.follow": "{name} folgt dir",
"notification.follow_request": "{name} möchte dir folgen", "notification.follow_request": "{name} möchte dir folgen",
"notification.mention": "{name} erwähnte dich", "notification.mention": "{name} erwähnte dich",
"notification.moderation-warning.learn_more": "Mehr erfahren",
"notification.moderation_warning": "Du wurdest von den Moderator*innen verwarnt",
"notification.moderation_warning.action_delete_statuses": "Einige deiner Beiträge sind entfernt worden.",
"notification.moderation_warning.action_disable": "Dein Konto wurde deaktiviert.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Einige deiner Beiträge wurden mit einer Inhaltswarnung versehen.",
"notification.moderation_warning.action_none": "Dein Konto ist von den Moderator*innen verwarnt worden.",
"notification.moderation_warning.action_sensitive": "Deine zukünftigen Beiträge werden mit einer Inhaltswarnung versehen.",
"notification.moderation_warning.action_silence": "Dein Konto wurde eingeschränkt.",
"notification.moderation_warning.action_suspend": "Dein Konto wurde gesperrt.",
"notification.own_poll": "Deine Umfrage ist beendet", "notification.own_poll": "Deine Umfrage ist beendet",
"notification.poll": "Eine Umfrage, an der du teilgenommen hast, ist beendet", "notification.poll": "Eine Umfrage, an der du teilgenommen hast, ist beendet",
"notification.reblog": "{name} teilte deinen Beitrag", "notification.reblog": "{name} teilte deinen Beitrag",
"notification.severed_relationships": "Beziehungen zu {name} getrennt", "notification.relationships_severance_event": "Verbindungen mit {name} verloren",
"notification.relationships_severance_event.account_suspension": "Ein Admin von {from} hat {target} gesperrt. Du wirst von diesem Profil keine Updates mehr erhalten und auch nicht mit ihm interagieren können.",
"notification.relationships_severance_event.domain_block": "Ein Admin von {from} hat {target} blockiert darunter {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst.",
"notification.relationships_severance_event.learn_more": "Mehr erfahren",
"notification.relationships_severance_event.user_domain_block": "Du hast {target} blockiert {followersCount} deiner Follower und {followingCount, plural, one {# Konto, dem} other {# Konten, denen}} du folgst, wurden entfernt.",
"notification.status": "{name} hat gerade etwas gepostet", "notification.status": "{name} hat gerade etwas gepostet",
"notification.update": "{name} bearbeitete einen Beitrag", "notification.update": "{name} bearbeitete einen Beitrag",
"notification_requests.accept": "Akzeptieren", "notification_requests.accept": "Akzeptieren",
@ -484,6 +502,8 @@
"notifications.column_settings.admin.sign_up": "Neue Registrierungen:", "notifications.column_settings.admin.sign_up": "Neue Registrierungen:",
"notifications.column_settings.alert": "Desktop-Benachrichtigungen", "notifications.column_settings.alert": "Desktop-Benachrichtigungen",
"notifications.column_settings.favourite": "Favoriten:", "notifications.column_settings.favourite": "Favoriten:",
"notifications.column_settings.filter_bar.advanced": "Alle Filterkategorien anzeigen",
"notifications.column_settings.filter_bar.category": "Filterleiste",
"notifications.column_settings.follow": "Neue Follower:", "notifications.column_settings.follow": "Neue Follower:",
"notifications.column_settings.follow_request": "Neue Follower-Anfragen:", "notifications.column_settings.follow_request": "Neue Follower-Anfragen:",
"notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.mention": "Erwähnungen:",
@ -529,11 +549,11 @@
"onboarding.follows.empty": "Bedauerlicherweise können aktuell keine Ergebnisse angezeigt werden. Du kannst die Suche verwenden oder den Reiter „Entdecken“ auswählen, um neue Leute zum Folgen zu finden oder du versuchst es später erneut.", "onboarding.follows.empty": "Bedauerlicherweise können aktuell keine Ergebnisse angezeigt werden. Du kannst die Suche verwenden oder den Reiter „Entdecken“ auswählen, um neue Leute zum Folgen zu finden oder du versuchst es später erneut.",
"onboarding.follows.lead": "Deine Startseite ist der primäre Anlaufpunkt, um Mastodon zu erleben. Je mehr Profilen du folgst, umso aktiver und interessanter wird sie. Damit du direkt loslegen kannst, gibt es hier ein paar Vorschläge:", "onboarding.follows.lead": "Deine Startseite ist der primäre Anlaufpunkt, um Mastodon zu erleben. Je mehr Profilen du folgst, umso aktiver und interessanter wird sie. Damit du direkt loslegen kannst, gibt es hier ein paar Vorschläge:",
"onboarding.follows.title": "Personalisiere deine Startseite", "onboarding.follows.title": "Personalisiere deine Startseite",
"onboarding.profile.discoverable": "Mein Profil auffindbar machen", "onboarding.profile.discoverable": "Mein Profil darf entdeckt werden",
"onboarding.profile.discoverable_hint": "Wenn du entdeckt werden möchtest, dann können deine Beiträge in Suchergebnissen und Trends erscheinen. Dein Profil kann ebenfalls anderen mit ähnlichen Interessen vorgeschlagen werden.", "onboarding.profile.discoverable_hint": "Wenn du entdeckt werden möchtest, dann können deine Beiträge in Suchergebnissen und Trends erscheinen. Dein Profil kann ebenfalls anderen mit ähnlichen Interessen vorgeschlagen werden.",
"onboarding.profile.display_name": "Anzeigename", "onboarding.profile.display_name": "Anzeigename",
"onboarding.profile.display_name_hint": "Dein richtiger Name oder dein Fantasiename …", "onboarding.profile.display_name_hint": "Dein richtiger Name oder dein Fantasiename …",
"onboarding.profile.lead": "Du kannst das später in den Einstellungen vervollständigen, wo noch mehr Anpassungsmöglichkeiten zur Verfügung stehen.", "onboarding.profile.lead": "Du kannst dein Profil später in den Einstellungen vervollständigen. Dort stehen weitere Anpassungsmöglichkeiten zur Verfügung.",
"onboarding.profile.note": "Über mich", "onboarding.profile.note": "Über mich",
"onboarding.profile.note_hint": "Du kannst andere @Profile erwähnen oder #Hashtags verwenden …", "onboarding.profile.note_hint": "Du kannst andere @Profile erwähnen oder #Hashtags verwenden …",
"onboarding.profile.save_and_continue": "Speichern und fortfahren", "onboarding.profile.save_and_continue": "Speichern und fortfahren",
@ -549,16 +569,16 @@
"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.",
"onboarding.steps.follow_people.title": "Personalisiere deine Startseite", "onboarding.steps.follow_people.title": "Personalisiere deine Startseite",
"onboarding.steps.publish_status.body": "Begrüße die Welt mit Text, Fotos, Videos oder Umfragen {emoji}", "onboarding.steps.publish_status.body": "Begrüße die Welt mit Text, Fotos, Videos oder Umfragen. {emoji}",
"onboarding.steps.publish_status.title": "Erstelle deinen ersten Beitrag", "onboarding.steps.publish_status.title": "Erstelle deinen ersten Beitrag",
"onboarding.steps.setup_profile.body": "Mit einem vollständigen Profil interagieren andere eher mit dir.", "onboarding.steps.setup_profile.body": "Mit einem vollständigen Profil interagieren andere eher mit dir.",
"onboarding.steps.setup_profile.title": "Personalisiere dein Profil", "onboarding.steps.setup_profile.title": "Personalisiere dein Profil",
"onboarding.steps.share_profile.body": "Lass deine Freund*innen wissen, wie sie dich auf Mastodon finden können", "onboarding.steps.share_profile.body": "Lass deine Freund*innen wissen, wie sie dich auf Mastodon finden können.",
"onboarding.steps.share_profile.title": "Teile dein Mastodon-Profil", "onboarding.steps.share_profile.title": "Teile dein Mastodon-Profil",
"onboarding.tips.2fa": "<strong>Wusstest du schon?</strong> Du kannst die Sicherheit deines Kontos erhöhen, indem du die Zwei-Faktor-Authentisierung in deinen Kontoeinstellungen aktivierst. Dafür ist keine Telefonnummer notwendig und es funktioniert jede beliebige TOTP-App!", "onboarding.tips.2fa": "<strong>Wusstest du schon?</strong> Du kannst die Sicherheit deines Kontos erhöhen, indem du die Zwei-Faktor-Authentisierung in deinen Kontoeinstellungen aktivierst. Dafür ist keine Telefonnummer notwendig und es funktioniert jede beliebige TOTP-App!",
"onboarding.tips.accounts_from_other_servers": "<strong>Wusstest du schon?</strong> Da Mastodon dezentralisiert ist, werden einige Profile, denen du begegnest, auf anderen Servern als deinem bereitgestellt. Und trotzdem kannst du uneingeschränkt mit ihnen interagieren! Der Servername befindet sich in der zweiten Hälfte ihres Profilnamens!", "onboarding.tips.accounts_from_other_servers": "<strong>Wusstest du schon?</strong> Da Mastodon dezentralisiert ist, werden einige Profile, denen du begegnest, auf anderen Servern als deinem bereitgestellt. Und trotzdem kannst du uneingeschränkt mit ihnen interagieren! Der Servername befindet sich in der zweiten Hälfte ihres Profilnamens!",
"onboarding.tips.migration": "<strong>Wusstest du schon?</strong> Wenn du das Gefühl hast, dass {domain} in Zukunft nicht die richtige Serverwahl für dich ist, kannst du auf einen anderen Mastodon-Server umziehen, ohne deine Follower zu verlieren. Du kannst sogar deinen eigenen Server betreiben!", "onboarding.tips.migration": "<strong>Wusstest du schon?</strong> Wenn du das Gefühl hast, dass {domain} in Zukunft nicht die richtige Serverwahl für dich ist, kannst du auf einen anderen Mastodon-Server umziehen, ohne deine Follower zu verlieren. Du kannst sogar deinen eigenen Server betreiben!",
"onboarding.tips.verification": "<strong>Wusstest du schon?</strong> Du kannst dein Konto verifizieren, indem du auf deiner Website auf dein Mastodon-Profil verlinkst und den Link deiner Website zu deinem Profil hinzufügst. Keine Gebühren oder Dokumente erforderlich!", "onboarding.tips.verification": "<strong>Wusstest du schon?</strong> Du kannst dein Konto verifizieren, indem du auf deiner Website auf dein Mastodon-Profil verlinkst und den Link deiner Website zu deinem Profil hinzufügst. Völlig kostenlos und ohne Dokumente einsenden zu müssen!",
"password_confirmation.exceeds_maxlength": "Passwortbestätigung überschreitet die maximal erlaubte Zeichenanzahl", "password_confirmation.exceeds_maxlength": "Passwortbestätigung überschreitet die maximal erlaubte Zeichenanzahl",
"password_confirmation.mismatching": "Passwortbestätigung stimmt nicht überein", "password_confirmation.mismatching": "Passwortbestätigung stimmt nicht überein",
"picture_in_picture.restore": "Zurücksetzen", "picture_in_picture.restore": "Zurücksetzen",
@ -588,12 +608,6 @@
"refresh": "Aktualisieren", "refresh": "Aktualisieren",
"regeneration_indicator.label": "Wird geladen …", "regeneration_indicator.label": "Wird geladen …",
"regeneration_indicator.sublabel": "Deine Startseite wird gerade vorbereitet!", "regeneration_indicator.sublabel": "Deine Startseite wird gerade vorbereitet!",
"relationship_severance_notification.purged_data": "von Administrator*innen entfernt",
"relationship_severance_notification.relationships": "{count, plural, one {# Beziehung} other {# Beziehungen}}",
"relationship_severance_notification.types.account_suspension": "Konto wurde gesperrt",
"relationship_severance_notification.types.domain_block": "Domain wurde gesperrt",
"relationship_severance_notification.types.user_domain_block": "Du hast diese Domain blockiert",
"relationship_severance_notification.view": "Anzeigen",
"relative_time.days": "{number} T.", "relative_time.days": "{number} T.",
"relative_time.full.days": "vor {number, plural, one {# Tag} other {# Tagen}}", "relative_time.full.days": "vor {number, plural, one {# Tag} other {# Tagen}}",
"relative_time.full.hours": "vor {number, plural, one {# Stunde} other {# Stunden}}", "relative_time.full.hours": "vor {number, plural, one {# Stunde} other {# Stunden}}",

View file

@ -89,6 +89,14 @@
"announcement.announcement": "Announcement", "announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)", "attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio", "audio.hide": "Hide audio",
"block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.",
"block_modal.show_less": "Show less",
"block_modal.show_more": "Show more",
"block_modal.they_cant_mention": "They can't mention or follow you.",
"block_modal.they_cant_see_posts": "They can't see your posts and you won't see theirs.",
"block_modal.they_will_know": "They can see that they're blocked.",
"block_modal.title": "Block user?",
"block_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"boost_modal.combo": "You can press {combo} to skip this next time", "boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.copy_stacktrace": "Copy error report", "bundle_column_error.copy_stacktrace": "Copy error report",
"bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.", "bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
@ -169,6 +177,7 @@
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.discard_edit_media.confirm": "Discard", "confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?", "confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.domain_block.confirm": "Block server",
"confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.",
"confirmations.edit.confirm": "Edit", "confirmations.edit.confirm": "Edit",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?", "confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
@ -200,6 +209,27 @@
"dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.", "dismissable_banner.explore_statuses": "These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralised network right now.", "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralised network right now.",
"dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.", "dismissable_banner.public_timeline": "These are the most recent public posts from people on the social web that people on {domain} follow.",
"domain_block_modal.block": "Block server",
"domain_block_modal.block_account_instead": "Block @{name} instead",
"domain_block_modal.they_can_interact_with_old_posts": "People from this server can interact with your old posts.",
"domain_block_modal.they_cant_follow": "Nobody from this server can follow you.",
"domain_block_modal.they_wont_know": "They won't know they've been blocked.",
"domain_block_modal.title": "Block domain?",
"domain_block_modal.you_will_lose_followers": "All your followers from this server will be removed.",
"domain_block_modal.you_wont_see_posts": "You won't see posts or notifications from users on this server.",
"domain_pill.activitypub_lets_connect": "It lets you connect and interact with people not just on Mastodon, but across different social apps too.",
"domain_pill.activitypub_like_language": "ActivityPub is like the language Mastodon speaks with other social networks.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Their handle:",
"domain_pill.their_server": "Their digital home, where all of their posts live.",
"domain_pill.their_username": "Their unique identifier on their server. Its possible to find users with the same username on different servers.",
"domain_pill.username": "Username",
"domain_pill.whats_in_a_handle": "What's in a handle?",
"domain_pill.who_they_are": "Since handles say who someone is and where they are, you can interact with people across the social web of <button>ActivityPub-powered platforms</button>.",
"domain_pill.who_you_are": "Because your handle says who you are and where you are, people can interact with you across the social web of <button>ActivityPub-powered platforms</button>.",
"domain_pill.your_handle": "Your handle:",
"domain_pill.your_server": "Your digital home, where all of your posts live. Dont like this one? Transfer servers at any time and bring your followers, too.",
"domain_pill.your_username": "Your unique identifier on this server. Its possible to find users with the same username on different servers.",
"embed.instructions": "Embed this post on your website by copying the code below.", "embed.instructions": "Embed this post on your website by copying the code below.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Here is what it will look like:",
"emoji_button.activity": "Activity", "emoji_button.activity": "Activity",
@ -236,6 +266,7 @@
"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.",
"empty_column.notification_requests": "All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.",
"empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.", "empty_column.notifications": "You don't have any notifications yet. When other people interact with you, you will see it here.",
"empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up",
"error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.",
@ -266,13 +297,22 @@
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one", "filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
"filter_modal.select_filter.title": "Filter this post", "filter_modal.select_filter.title": "Filter this post",
"filter_modal.title.status": "Filter a post", "filter_modal.title.status": "Filter a post",
"filtered_notifications_banner.mentions": "{count, plural, one {mention} other {mentions}}",
"filtered_notifications_banner.pending_requests": "Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know",
"filtered_notifications_banner.title": "Filtered notifications",
"firehose.all": "All", "firehose.all": "All",
"firehose.local": "This server", "firehose.local": "This server",
"firehose.remote": "Other servers", "firehose.remote": "Other servers",
"follow_request.authorize": "Authorise", "follow_request.authorize": "Authorise",
"follow_request.reject": "Reject", "follow_request.reject": "Reject",
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"follow_suggestions.curated_suggestion": "Staff pick",
"follow_suggestions.dismiss": "Don't show again", "follow_suggestions.dismiss": "Don't show again",
"follow_suggestions.hints.featured": "This profile has been hand-picked by the {domain} team.",
"follow_suggestions.hints.friends_of_friends": "This profile is popular among the people you follow.",
"follow_suggestions.hints.most_followed": "This profile is one of the most followed on {domain}.",
"follow_suggestions.hints.most_interactions": "This profile has been recently getting a lot of attention on {domain}.",
"follow_suggestions.hints.similar_to_recently_followed": "This profile is similar to the profiles you have most recently followed.",
"follow_suggestions.personalized_suggestion": "Personalised suggestion", "follow_suggestions.personalized_suggestion": "Personalised suggestion",
"follow_suggestions.popular_suggestion": "Popular suggestion", "follow_suggestions.popular_suggestion": "Popular suggestion",
"follow_suggestions.view_all": "View all", "follow_suggestions.view_all": "View all",
@ -388,6 +428,15 @@
"loading_indicator.label": "Loading…", "loading_indicator.label": "Loading…",
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}", "media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
"mute_modal.hide_from_notifications": "Hide from notifications",
"mute_modal.hide_options": "Hide options",
"mute_modal.indefinite": "Until I unmute them",
"mute_modal.show_options": "Show options",
"mute_modal.they_can_mention_and_follow": "They can mention and follow you, but you won't see them.",
"mute_modal.they_wont_know": "They won't know they've been muted.",
"mute_modal.title": "Mute user?",
"mute_modal.you_wont_see_mentions": "You won't see posts that mention them.",
"mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.",
"navigation_bar.about": "About", "navigation_bar.about": "About",
"navigation_bar.advanced_interface": "Open in advanced web interface", "navigation_bar.advanced_interface": "Open in advanced web interface",
"navigation_bar.blocks": "Blocked users", "navigation_bar.blocks": "Blocked users",
@ -423,14 +472,25 @@
"notification.own_poll": "Your poll has ended", "notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended", "notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your status", "notification.reblog": "{name} boosted your status",
"notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
"notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.relationships_severance_event.learn_more": "Learn more",
"notification.relationships_severance_event.user_domain_block": "You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.status": "{name} just posted", "notification.status": "{name} just posted",
"notification.update": "{name} edited a post", "notification.update": "{name} edited a post",
"notification_requests.accept": "Accept",
"notification_requests.dismiss": "Dismiss",
"notification_requests.notifications_from": "Notifications from {name}",
"notification_requests.title": "Filtered notifications",
"notifications.clear": "Clear notifications", "notifications.clear": "Clear notifications",
"notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?",
"notifications.column_settings.admin.report": "New reports:", "notifications.column_settings.admin.report": "New reports:",
"notifications.column_settings.admin.sign_up": "New sign-ups:", "notifications.column_settings.admin.sign_up": "New sign-ups:",
"notifications.column_settings.alert": "Desktop notifications", "notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.favourite": "Favourites:", "notifications.column_settings.favourite": "Favourites:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
"notifications.column_settings.follow": "New followers:", "notifications.column_settings.follow": "New followers:",
"notifications.column_settings.follow_request": "New follow requests:", "notifications.column_settings.follow_request": "New follow requests:",
"notifications.column_settings.mention": "Mentions:", "notifications.column_settings.mention": "Mentions:",
@ -456,6 +516,15 @@
"notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request", "notifications.permission_denied": "Desktop notifications are unavailable due to previously denied browser permissions request",
"notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before", "notifications.permission_denied_alert": "Desktop notifications can't be enabled, as browser permission has been denied before",
"notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.", "notifications.permission_required": "Desktop notifications are unavailable because the required permission has not been granted.",
"notifications.policy.filter_new_accounts.hint": "Created within the past {days, plural, one {one day} other {# days}}",
"notifications.policy.filter_new_accounts_title": "New accounts",
"notifications.policy.filter_not_followers_hint": "Including people who have been following you fewer than {days, plural, one {one day} other {# days}}",
"notifications.policy.filter_not_followers_title": "People not following you",
"notifications.policy.filter_not_following_hint": "Until you manually approve them",
"notifications.policy.filter_not_following_title": "People you don't follow",
"notifications.policy.filter_private_mentions_hint": "Filtered unless it's in reply to your own mention or if you follow the sender",
"notifications.policy.filter_private_mentions_title": "Unsolicited private mentions",
"notifications.policy.title": "Filter out notifications from…",
"notifications_permission_banner.enable": "Enable desktop notifications", "notifications_permission_banner.enable": "Enable desktop notifications",
"notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.", "notifications_permission_banner.how_to_control": "To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled.",
"notifications_permission_banner.title": "Never miss a thing", "notifications_permission_banner.title": "Never miss a thing",
@ -632,9 +701,11 @@
"status.direct": "Privately mention @{name}", "status.direct": "Privately mention @{name}",
"status.direct_indicator": "Private mention", "status.direct_indicator": "Private mention",
"status.edit": "Edit", "status.edit": "Edit",
"status.edited": "Last edited {date}",
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}", "status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Embed", "status.embed": "Embed",
"status.favourite": "Favourite", "status.favourite": "Favourite",
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
"status.filter": "Filter this post", "status.filter": "Filter this post",
"status.filtered": "Filtered", "status.filtered": "Filtered",
"status.hide": "Hide post", "status.hide": "Hide post",
@ -655,6 +726,7 @@
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility", "status.reblog_private": "Boost with original visibility",
"status.reblogged_by": "{name} boosted", "status.reblogged_by": "{name} boosted",
"status.reblogs": "{count, plural, one {boost} other {boosts}}",
"status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.", "status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
"status.redraft": "Delete & re-draft", "status.redraft": "Delete & re-draft",
"status.remove_bookmark": "Remove bookmark", "status.remove_bookmark": "Remove bookmark",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one", "filter_modal.select_filter.subtitle": "Use an existing category or create a new one",
"filter_modal.select_filter.title": "Filter this post", "filter_modal.select_filter.title": "Filter this post",
"filter_modal.title.status": "Filter a post", "filter_modal.title.status": "Filter a post",
"filtered_notifications_banner.mentions": "{count, plural, one {mention} other {mentions}}",
"filtered_notifications_banner.pending_requests": "Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know", "filtered_notifications_banner.pending_requests": "Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know",
"filtered_notifications_banner.title": "Filtered notifications", "filtered_notifications_banner.title": "Filtered notifications",
"firehose.all": "All", "firehose.all": "All",
@ -307,6 +308,8 @@
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.", "follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"follow_suggestions.curated_suggestion": "Staff pick", "follow_suggestions.curated_suggestion": "Staff pick",
"follow_suggestions.dismiss": "Don't show again", "follow_suggestions.dismiss": "Don't show again",
"follow_suggestions.featured_longer": "Hand-picked by the {domain} team",
"follow_suggestions.friends_of_friends_longer": "Popular among people you follow",
"follow_suggestions.hints.featured": "This profile has been hand-picked by the {domain} team.", "follow_suggestions.hints.featured": "This profile has been hand-picked by the {domain} team.",
"follow_suggestions.hints.friends_of_friends": "This profile is popular among the people you follow.", "follow_suggestions.hints.friends_of_friends": "This profile is popular among the people you follow.",
"follow_suggestions.hints.most_followed": "This profile is one of the most followed on {domain}.", "follow_suggestions.hints.most_followed": "This profile is one of the most followed on {domain}.",
@ -314,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "This profile is similar to the profiles you have most recently followed.", "follow_suggestions.hints.similar_to_recently_followed": "This profile is similar to the profiles you have most recently followed.",
"follow_suggestions.personalized_suggestion": "Personalized suggestion", "follow_suggestions.personalized_suggestion": "Personalized suggestion",
"follow_suggestions.popular_suggestion": "Popular suggestion", "follow_suggestions.popular_suggestion": "Popular suggestion",
"follow_suggestions.popular_suggestion_longer": "Popular on {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Similar to profiles you recently followed",
"follow_suggestions.view_all": "View all", "follow_suggestions.view_all": "View all",
"follow_suggestions.who_to_follow": "Who to follow", "follow_suggestions.who_to_follow": "Who to follow",
"followed_tags": "Followed hashtags", "followed_tags": "Followed hashtags",
@ -468,10 +473,23 @@
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you", "notification.follow_request": "{name} has requested to follow you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",
"notification.moderation-warning.learn_more": "Learn more",
"notification.moderation_warning": "Your have received a moderation warning",
"notification.moderation_warning.action_delete_statuses": "Some of your posts have been removed.",
"notification.moderation_warning.action_disable": "Your account has been disabled.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Some of your posts have been marked as sensitive.",
"notification.moderation_warning.action_none": "Your account has received a moderation warning.",
"notification.moderation_warning.action_sensitive": "Your posts will be marked as sensitive from now on.",
"notification.moderation_warning.action_silence": "Your account has been limited.",
"notification.moderation_warning.action_suspend": "Your account has been suspended.",
"notification.own_poll": "Your poll has ended", "notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you have voted in has ended", "notification.poll": "A poll you have voted in has ended",
"notification.reblog": "{name} boosted your post", "notification.reblog": "{name} boosted your post",
"notification.severed_relationships": "Relationships with {name} severed", "notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
"notification.relationships_severance_event.domain_block": "An admin from {from} has blocked {target}, including {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.relationships_severance_event.learn_more": "Learn more",
"notification.relationships_severance_event.user_domain_block": "You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.",
"notification.status": "{name} just posted", "notification.status": "{name} just posted",
"notification.update": "{name} edited a post", "notification.update": "{name} edited a post",
"notification_requests.accept": "Accept", "notification_requests.accept": "Accept",
@ -590,12 +608,6 @@
"refresh": "Refresh", "refresh": "Refresh",
"regeneration_indicator.label": "Loading…", "regeneration_indicator.label": "Loading…",
"regeneration_indicator.sublabel": "Your home feed is being prepared!", "regeneration_indicator.sublabel": "Your home feed is being prepared!",
"relationship_severance_notification.purged_data": "purged by administrators",
"relationship_severance_notification.relationships": "{count, plural, one {# relationship} other {# relationships}}",
"relationship_severance_notification.types.account_suspension": "Account has been suspended",
"relationship_severance_notification.types.domain_block": "Domain has been suspended",
"relationship_severance_notification.types.user_domain_block": "You blocked this domain",
"relationship_severance_notification.view": "View",
"relative_time.days": "{number}d", "relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# day} other {# days}} ago", "relative_time.full.days": "{number, plural, one {# day} other {# days}} ago",
"relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago", "relative_time.full.hours": "{number, plural, one {# hour} other {# hours}} ago",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva", "filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva",
"filter_modal.select_filter.title": "Filtrar este mensaje", "filter_modal.select_filter.title": "Filtrar este mensaje",
"filter_modal.title.status": "Filtrar un mensaje", "filter_modal.title.status": "Filtrar un mensaje",
"filtered_notifications_banner.mentions": "{count, plural, one {mención} other {menciones}}",
"filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer", "filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer",
"filtered_notifications_banner.title": "Notificaciones filtradas", "filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todos", "firehose.all": "Todos",
@ -427,7 +428,7 @@
"loading_indicator.label": "Cargando…", "loading_indicator.label": "Cargando…",
"media_gallery.toggle_visible": "Ocultar {number, plural, one {imagen} other {imágenes}}", "media_gallery.toggle_visible": "Ocultar {number, plural, one {imagen} other {imágenes}}",
"moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.",
"mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_from_notifications": "Ocultar en las notificaciones",
"mute_modal.hide_options": "Ocultar opciones", "mute_modal.hide_options": "Ocultar opciones",
"mute_modal.indefinite": "Hasta que deje de silenciarlos", "mute_modal.indefinite": "Hasta que deje de silenciarlos",
"mute_modal.show_options": "Mostrar opciones", "mute_modal.show_options": "Mostrar opciones",
@ -471,7 +472,11 @@
"notification.own_poll": "Tu encuesta finalizó", "notification.own_poll": "Tu encuesta finalizó",
"notification.poll": "Finalizó una encuesta en la que votaste", "notification.poll": "Finalizó una encuesta en la que votaste",
"notification.reblog": "{name} adhirió a tu mensaje", "notification.reblog": "{name} adhirió a tu mensaje",
"notification.severed_relationships": "Relaciones con {name} cortadas", "notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} suspendió a {target}, lo que significa que ya no podés recibir actualizaciones de esa cuenta o interactuar con la misma.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} bloqueó a {target}, incluyendo {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que seguís.",
"notification.relationships_severance_event.learn_more": "Aprendé más",
"notification.relationships_severance_event.user_domain_block": "Bloqueaste a {target}, eliminando {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que seguís.",
"notification.status": "{name} acaba de enviar un mensaje", "notification.status": "{name} acaba de enviar un mensaje",
"notification.update": "{name} editó un mensaje", "notification.update": "{name} editó un mensaje",
"notification_requests.accept": "Aceptar", "notification_requests.accept": "Aceptar",
@ -484,6 +489,8 @@
"notifications.column_settings.admin.sign_up": "Nuevos registros:", "notifications.column_settings.admin.sign_up": "Nuevos registros:",
"notifications.column_settings.alert": "Notificaciones de escritorio", "notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:", "notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
@ -588,12 +595,6 @@
"refresh": "Refrescar", "refresh": "Refrescar",
"regeneration_indicator.label": "Cargando…", "regeneration_indicator.label": "Cargando…",
"regeneration_indicator.sublabel": "¡Se está preparando tu línea temporal principal!", "regeneration_indicator.sublabel": "¡Se está preparando tu línea temporal principal!",
"relationship_severance_notification.purged_data": "purgada por administradores",
"relationship_severance_notification.relationships": "{count, plural, one {# relación} other {# relaciones}}",
"relationship_severance_notification.types.account_suspension": "La cuenta fue suspendida",
"relationship_severance_notification.types.domain_block": "El dominio fue suspendido",
"relationship_severance_notification.types.user_domain_block": "Bloqueaste este dominio",
"relationship_severance_notification.view": "Ver",
"relative_time.days": "{number}d", "relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural,one {hace # día} other {hace # días}}", "relative_time.full.days": "{number, plural,one {hace # día} other {hace # días}}",
"relative_time.full.hours": "{number, plural,one {hace # hora} other {hace # horas}}", "relative_time.full.hours": "{number, plural,one {hace # hora} other {hace # horas}}",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva", "filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva",
"filter_modal.select_filter.title": "Filtrar esta publicación", "filter_modal.select_filter.title": "Filtrar esta publicación",
"filter_modal.title.status": "Filtrar una publicación", "filter_modal.title.status": "Filtrar una publicación",
"filtered_notifications_banner.mentions": "{count, plural, one {mención} other {menciones}}",
"filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer", "filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer",
"filtered_notifications_banner.title": "Notificaciones filtradas", "filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todas", "firehose.all": "Todas",
@ -471,6 +472,11 @@
"notification.own_poll": "Tu encuesta ha terminado", "notification.own_poll": "Tu encuesta ha terminado",
"notification.poll": "Una encuesta en la que has votado ha terminado", "notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.reblog": "{name} ha retooteado tu estado", "notification.reblog": "{name} ha retooteado tu estado",
"notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspendido {target}, lo que significa que ya no puedes recibir actualizaciones de sus cuentas o interactuar con ellas.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} ha bloqueado {target}, incluyendo {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
"notification.relationships_severance_event.learn_more": "Más información",
"notification.relationships_severance_event.user_domain_block": "Has bloqueado {target}, eliminando {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
"notification.status": "{name} acaba de publicar", "notification.status": "{name} acaba de publicar",
"notification.update": "{name} editó una publicación", "notification.update": "{name} editó una publicación",
"notification_requests.accept": "Aceptar", "notification_requests.accept": "Aceptar",
@ -483,6 +489,8 @@
"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.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:", "notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",

View file

@ -297,6 +297,7 @@
"filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva", "filter_modal.select_filter.subtitle": "Usar una categoría existente o crear una nueva",
"filter_modal.select_filter.title": "Filtrar esta publicación", "filter_modal.select_filter.title": "Filtrar esta publicación",
"filter_modal.title.status": "Filtrar una publicación", "filter_modal.title.status": "Filtrar una publicación",
"filtered_notifications_banner.mentions": "{count, plural, one {mención} other {menciones}}",
"filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer", "filtered_notifications_banner.pending_requests": "Notificaciones de {count, plural, =0 {nadie} one {una persona} other {# personas}} que podrías conocer",
"filtered_notifications_banner.title": "Notificaciones filtradas", "filtered_notifications_banner.title": "Notificaciones filtradas",
"firehose.all": "Todas", "firehose.all": "Todas",
@ -471,6 +472,11 @@
"notification.own_poll": "Tu encuesta ha terminado", "notification.own_poll": "Tu encuesta ha terminado",
"notification.poll": "Una encuesta en la que has votado ha terminado", "notification.poll": "Una encuesta en la que has votado ha terminado",
"notification.reblog": "{name} ha impulsado tu publicación", "notification.reblog": "{name} ha impulsado tu publicación",
"notification.relationships_severance_event": "Conexiones perdidas con {name}",
"notification.relationships_severance_event.account_suspension": "Un administrador de {from} ha suspendido {target}, lo que significa que ya no puedes recibir actualizaciones de sus cuentas o interactuar con ellas.",
"notification.relationships_severance_event.domain_block": "Un administrador de {from} ha bloqueado {target}, incluyendo {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
"notification.relationships_severance_event.learn_more": "Más información",
"notification.relationships_severance_event.user_domain_block": "Has bloqueado {target}, eliminando {followersCount} de tus seguidores y {followingCount, plural, one {# cuenta} other {# cuentas}} que sigues.",
"notification.status": "{name} acaba de publicar", "notification.status": "{name} acaba de publicar",
"notification.update": "{name} editó una publicación", "notification.update": "{name} editó una publicación",
"notification_requests.accept": "Aceptar", "notification_requests.accept": "Aceptar",
@ -483,6 +489,8 @@
"notifications.column_settings.admin.sign_up": "Nuevos registros:", "notifications.column_settings.admin.sign_up": "Nuevos registros:",
"notifications.column_settings.alert": "Notificaciones de escritorio", "notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.favourite": "Favoritos:", "notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
"notifications.column_settings.follow": "Nuevos seguidores:", "notifications.column_settings.follow": "Nuevos seguidores:",
"notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:", "notifications.column_settings.follow_request": "Nuevas solicitudes de seguimiento:",
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
@ -634,7 +642,7 @@
"report.statuses.subtitle": "Selecciona todos los que correspondan", "report.statuses.subtitle": "Selecciona todos los que correspondan",
"report.statuses.title": "¿Hay alguna publicación que respalde este informe?", "report.statuses.title": "¿Hay alguna publicación que respalde este informe?",
"report.submit": "Enviar", "report.submit": "Enviar",
"report.target": "Reportando", "report.target": "Reportando {target}",
"report.thanks.take_action": "Aquí están tus opciones para controlar lo que ves en Mastodon:", "report.thanks.take_action": "Aquí están tus opciones para controlar lo que ves en Mastodon:",
"report.thanks.take_action_actionable": "Mientras revisamos esto, puedes tomar medidas contra @{name}:", "report.thanks.take_action_actionable": "Mientras revisamos esto, puedes tomar medidas contra @{name}:",
"report.thanks.title": "¿No quieres esto?", "report.thanks.title": "¿No quieres esto?",

View file

@ -92,7 +92,11 @@
"block_modal.remote_users_caveat": "Serverile {domain} edastatakse palve otsust järgida. Ometi pole see tagatud, kuna mõned serverid võivad blokeeringuid käsitleda omal moel. Avalikud postitused võivad tuvastamata kasutajatele endiselt näha olla.", "block_modal.remote_users_caveat": "Serverile {domain} edastatakse palve otsust järgida. Ometi pole see tagatud, kuna mõned serverid võivad blokeeringuid käsitleda omal moel. Avalikud postitused võivad tuvastamata kasutajatele endiselt näha olla.",
"block_modal.show_less": "Kuva vähem", "block_modal.show_less": "Kuva vähem",
"block_modal.show_more": "Kuva rohkem", "block_modal.show_more": "Kuva rohkem",
"block_modal.they_cant_mention": "Ta ei saa mainida sind ega jälgida.",
"block_modal.they_cant_see_posts": "Ta ei näe sinu postitusi ja sa ei näe tema omi.",
"block_modal.they_will_know": "Ta näeb, et ta on blokeeritud.",
"block_modal.title": "Blokeeri kasutaja?", "block_modal.title": "Blokeeri kasutaja?",
"block_modal.you_wont_see_mentions": "Sa ei näe postitusi, mis mainivad teda.",
"boost_modal.combo": "Vajutades {combo}, saab selle edaspidi vahele jätta", "boost_modal.combo": "Vajutades {combo}, saab selle edaspidi vahele jätta",
"bundle_column_error.copy_stacktrace": "Kopeeri veateade", "bundle_column_error.copy_stacktrace": "Kopeeri veateade",
"bundle_column_error.error.body": "Soovitud lehte ei õnnestunud esitada. See võib olla meie koodiviga või probleem brauseri ühilduvusega.", "bundle_column_error.error.body": "Soovitud lehte ei õnnestunud esitada. See võib olla meie koodiviga või probleem brauseri ühilduvusega.",
@ -206,8 +210,26 @@
"dismissable_banner.explore_tags": "Need sildid siit ja teistes serveritest detsentraliseeritud võrgus koguvad tähelepanu just praegu selles serveris.", "dismissable_banner.explore_tags": "Need sildid siit ja teistes serveritest detsentraliseeritud võrgus koguvad tähelepanu just praegu selles serveris.",
"dismissable_banner.public_timeline": "Need on kõige uuemad avalikud postitused inimestelt sotsiaalvõrgustikus, mida {domain} inimesed jälgivad.", "dismissable_banner.public_timeline": "Need on kõige uuemad avalikud postitused inimestelt sotsiaalvõrgustikus, mida {domain} inimesed jälgivad.",
"domain_block_modal.block": "Blokeeri server", "domain_block_modal.block": "Blokeeri server",
"domain_block_modal.block_account_instead": "Selle asemel blokeeri @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Inimesed sellest serverist saavad interakteeruda sinu vanade postitustega.",
"domain_block_modal.they_cant_follow": "Sellest serverist ei saa keegi sind jälgida.",
"domain_block_modal.they_wont_know": "Nad ei tea, et nad on blokeeritud.",
"domain_block_modal.title": "Blokeerida domeen?",
"domain_block_modal.you_will_lose_followers": "Kõik sinu sellest serverist pärit jälgijad eemaldatakse.",
"domain_block_modal.you_wont_see_posts": "Sa ei näe selle serveri kasutajate postitusi ega teavitusi.",
"domain_pill.activitypub_lets_connect": "See võimaldab sul ühenduda inimestega ja nendega suhelda mitte ainult Mastodonis, vaid ka teistes suhtlusrakendustes.",
"domain_pill.activitypub_like_language": "ActivityPub on nagu keel, mida Mastodon räägib teiste suhtlusvõrgustikega.",
"domain_pill.server": "Server", "domain_pill.server": "Server",
"domain_pill.their_handle": "Tema tunnus:",
"domain_pill.their_server": "Tema digitaalne kodu, kus kõik tema postitused on.",
"domain_pill.their_username": "Tema unikaalne tunnus tema serveris. On võimalik, et mingites teistes serverites on sama kasutajanimega kasutajaid.",
"domain_pill.username": "Kasutajanimi", "domain_pill.username": "Kasutajanimi",
"domain_pill.whats_in_a_handle": "Mis on tunnuses?",
"domain_pill.who_they_are": "Kuna tunnus ütleb, kes keegi on ja kus, saad suhelda inimestega üle <button>ActivityPub-poolt toetatud sotsiaalvõrkude platvormide</button>.",
"domain_pill.who_you_are": "Kuna tunnus ütleb, kes sa oled ja kus, saavad inimesed sinuga suhelda üle <button>ActivityPub-poolt toetatud sotsiaalvõrkude platvormide</button>.",
"domain_pill.your_handle": "Sinu tunnus:",
"domain_pill.your_server": "Sinu digitaalne kodu, kus on kõik sinu postitused. Sulle ei meeldi see? Vaheta mistahes ajal serverit ja võta jälgijad ka.",
"domain_pill.your_username": "Sinu unikaalne identifikaator siin serveris. On võimalik, et leiad teistes serverites samasuguse kasutajanimega kasutajaid.",
"embed.instructions": "Lisa see postitus oma veebilehele, kopeerides alloleva koodi.", "embed.instructions": "Lisa see postitus oma veebilehele, kopeerides alloleva koodi.",
"embed.preview": "Nii näeb see välja:", "embed.preview": "Nii näeb see välja:",
"emoji_button.activity": "Tegevus", "emoji_button.activity": "Tegevus",
@ -244,6 +266,7 @@
"empty_column.list": "Siin loetelus pole veel midagi. Kui loetelu liikmed teevad uusi postitusi, näed neid siin.", "empty_column.list": "Siin loetelus pole veel midagi. Kui loetelu liikmed teevad uusi postitusi, näed neid siin.",
"empty_column.lists": "Pole veel ühtegi nimekirja. Kui lood mõne, näed neid siin.", "empty_column.lists": "Pole veel ühtegi nimekirja. Kui lood mõne, näed neid siin.",
"empty_column.mutes": "Sa pole veel ühtegi kasutajat vaigistanud.", "empty_column.mutes": "Sa pole veel ühtegi kasutajat vaigistanud.",
"empty_column.notification_requests": "Kõik tühi! Siin pole mitte midagi. Kui saad uusi teavitusi, ilmuvad need siin vastavalt sinu seadistustele.",
"empty_column.notifications": "Ei ole veel teateid. Kui keegi suhtleb sinuga, näed seda siin.", "empty_column.notifications": "Ei ole veel teateid. Kui keegi suhtleb sinuga, näed seda siin.",
"empty_column.public": "Siin pole midagi! Kirjuta midagi avalikku või jälgi ise kasutajaid täitmaks seda ruumi", "empty_column.public": "Siin pole midagi! Kirjuta midagi avalikku või jälgi ise kasutajaid täitmaks seda ruumi",
"error.unexpected_crash.explanation": "Meie poolse probleemi või veebilehitseja ühilduvusprobleemi tõttu ei suutnud me seda lehekülge korrektselt näidata.", "error.unexpected_crash.explanation": "Meie poolse probleemi või veebilehitseja ühilduvusprobleemi tõttu ei suutnud me seda lehekülge korrektselt näidata.",
@ -275,6 +298,7 @@
"filter_modal.select_filter.title": "Filtreeri seda postitust", "filter_modal.select_filter.title": "Filtreeri seda postitust",
"filter_modal.title.status": "Postituse filtreerimine", "filter_modal.title.status": "Postituse filtreerimine",
"filtered_notifications_banner.pending_requests": "Teateid {count, plural, =0 {mitte üheltki} one {ühelt} other {#}} inimeselt, keda võid teada", "filtered_notifications_banner.pending_requests": "Teateid {count, plural, =0 {mitte üheltki} one {ühelt} other {#}} inimeselt, keda võid teada",
"filtered_notifications_banner.title": "Filtreeritud teavitused",
"firehose.all": "Kõik", "firehose.all": "Kõik",
"firehose.local": "See server", "firehose.local": "See server",
"firehose.remote": "Teised serverid", "firehose.remote": "Teised serverid",
@ -403,8 +427,15 @@
"loading_indicator.label": "Laadimine…", "loading_indicator.label": "Laadimine…",
"media_gallery.toggle_visible": "{number, plural, one {Varja pilt} other {Varja pildid}}", "media_gallery.toggle_visible": "{number, plural, one {Varja pilt} other {Varja pildid}}",
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.", "moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
"mute_modal.hide_from_notifications": "Peida teavituste hulgast",
"mute_modal.hide_options": "Peida valikud", "mute_modal.hide_options": "Peida valikud",
"mute_modal.indefinite": "Kuni eemaldan neilt vaigistuse",
"mute_modal.show_options": "Kuva valikud", "mute_modal.show_options": "Kuva valikud",
"mute_modal.they_can_mention_and_follow": "Ta saab sind mainida ja sind jälgida, kuid sa ei näe teda.",
"mute_modal.they_wont_know": "Ta ei tea, et ta on vaigistatud.",
"mute_modal.title": "Vaigistada kasutaja?",
"mute_modal.you_wont_see_mentions": "Sa ei näe postitusi, mis teda mainivad.",
"mute_modal.you_wont_see_posts": "Ta näeb jätkuvalt sinu postitusi, kuid sa ei näe tema omi.",
"navigation_bar.about": "Teave", "navigation_bar.about": "Teave",
"navigation_bar.advanced_interface": "Ava kohandatud veebiliides", "navigation_bar.advanced_interface": "Ava kohandatud veebiliides",
"navigation_bar.blocks": "Blokeeritud kasutajad", "navigation_bar.blocks": "Blokeeritud kasutajad",
@ -440,16 +471,25 @@
"notification.own_poll": "Su küsitlus on lõppenud", "notification.own_poll": "Su küsitlus on lõppenud",
"notification.poll": "Küsitlus, milles osalesid, on lõppenud", "notification.poll": "Küsitlus, milles osalesid, on lõppenud",
"notification.reblog": "{name} jagas edasi postitust", "notification.reblog": "{name} jagas edasi postitust",
"notification.relationships_severance_event": "Kadunud ühendus kasutajaga {name}",
"notification.relationships_severance_event.account_suspension": "{from} admin on kustutanud {target}, mis tähendab, et sa ei saa enam neilt uuendusi või suhelda nendega.",
"notification.relationships_severance_event.domain_block": "{from} admin on blokeerinud {target}, sealhulgas {followersCount} sinu jälgijat ja {followingCount, plural, one {# konto} other {# kontot}}, mida jälgid.",
"notification.relationships_severance_event.learn_more": "Saa rohkem teada",
"notification.relationships_severance_event.user_domain_block": "Blokeerisid {target}, eemaldades oma jälgijate hulgast {followersCount} ja jälgitavate hulgast {followingCount, plural, one {# konto} other {# kontot}}.",
"notification.status": "{name} just postitas", "notification.status": "{name} just postitas",
"notification.update": "{name} muutis postitust", "notification.update": "{name} muutis postitust",
"notification_requests.accept": "Nõus", "notification_requests.accept": "Nõus",
"notification_requests.dismiss": "Hülga", "notification_requests.dismiss": "Hülga",
"notification_requests.notifications_from": "Teavitus kasutajalt {name}",
"notification_requests.title": "Filtreeritud teavitused",
"notifications.clear": "Puhasta teated", "notifications.clear": "Puhasta teated",
"notifications.clear_confirmation": "Oled kindel, et soovid püsivalt kõik oma teated eemaldada?", "notifications.clear_confirmation": "Oled kindel, et soovid püsivalt kõik oma teated eemaldada?",
"notifications.column_settings.admin.report": "Uued teavitused:", "notifications.column_settings.admin.report": "Uued teavitused:",
"notifications.column_settings.admin.sign_up": "Uued kasutajad:", "notifications.column_settings.admin.sign_up": "Uued kasutajad:",
"notifications.column_settings.alert": "Töölauateated", "notifications.column_settings.alert": "Töölauateated",
"notifications.column_settings.favourite": "Lemmikud:", "notifications.column_settings.favourite": "Lemmikud:",
"notifications.column_settings.filter_bar.advanced": "Näita kõiki kategooriaid",
"notifications.column_settings.filter_bar.category": "Kiirfiltri riba",
"notifications.column_settings.follow": "Uued jälgijad:", "notifications.column_settings.follow": "Uued jälgijad:",
"notifications.column_settings.follow_request": "Uued jälgimistaotlused:", "notifications.column_settings.follow_request": "Uued jälgimistaotlused:",
"notifications.column_settings.mention": "Mainimised:", "notifications.column_settings.mention": "Mainimised:",
@ -475,7 +515,15 @@
"notifications.permission_denied": "Töölauamärguanded pole saadaval, kuna eelnevalt keelduti lehitsejale teavituste luba andmast", "notifications.permission_denied": "Töölauamärguanded pole saadaval, kuna eelnevalt keelduti lehitsejale teavituste luba andmast",
"notifications.permission_denied_alert": "Töölaua märguandeid ei saa lubada, kuna brauseri luba on varem keeldutud", "notifications.permission_denied_alert": "Töölaua märguandeid ei saa lubada, kuna brauseri luba on varem keeldutud",
"notifications.permission_required": "Töölaua märguanded ei ole saadaval, kuna vajalik luba pole antud.", "notifications.permission_required": "Töölaua märguanded ei ole saadaval, kuna vajalik luba pole antud.",
"notifications.policy.filter_new_accounts.hint": "Loodud viimase {days, plural, one {ühe päeva} other {# päeva}} jooksul",
"notifications.policy.filter_new_accounts_title": "Uued kontod", "notifications.policy.filter_new_accounts_title": "Uued kontod",
"notifications.policy.filter_not_followers_hint": "Kaasates kasutajad, kes on sind jälginud vähem kui {days, plural, one {ühe päeva} other {# päeva}}",
"notifications.policy.filter_not_followers_title": "Sind mittejälgivad kasutajad",
"notifications.policy.filter_not_following_hint": "Kuni sa nad käsitsi kinnitad",
"notifications.policy.filter_not_following_title": "Inimesed, keda sa ei jälgi",
"notifications.policy.filter_private_mentions_hint": "Filtreeritud, kui see pole vastus sinupoolt mainimisele või kui jälgid saatjat",
"notifications.policy.filter_private_mentions_title": "Soovimatud privaatsed mainimised",
"notifications.policy.title": "Filtreeri välja teavitused kohast…",
"notifications_permission_banner.enable": "Luba töölaua märguanded", "notifications_permission_banner.enable": "Luba töölaua märguanded",
"notifications_permission_banner.how_to_control": "Et saada teateid, ajal mil Mastodon pole avatud, luba töölauamärguanded. Saad täpselt määrata, mis tüüpi tegevused tekitavad märguandeid, kasutates peale teadaannete sisse lülitamist üleval olevat nuppu {icon}.", "notifications_permission_banner.how_to_control": "Et saada teateid, ajal mil Mastodon pole avatud, luba töölauamärguanded. Saad täpselt määrata, mis tüüpi tegevused tekitavad märguandeid, kasutates peale teadaannete sisse lülitamist üleval olevat nuppu {icon}.",
"notifications_permission_banner.title": "Ära jää millestki ilma", "notifications_permission_banner.title": "Ära jää millestki ilma",
@ -652,9 +700,11 @@
"status.direct": "Maini privaatselt @{name}", "status.direct": "Maini privaatselt @{name}",
"status.direct_indicator": "Privaatne mainimine", "status.direct_indicator": "Privaatne mainimine",
"status.edit": "Muuda", "status.edit": "Muuda",
"status.edited": "Viimati muudetud {date}",
"status.edited_x_times": "Muudetud {count, plural, one{{count} kord} other {{count} korda}}", "status.edited_x_times": "Muudetud {count, plural, one{{count} kord} other {{count} korda}}",
"status.embed": "Manustamine", "status.embed": "Manustamine",
"status.favourite": "Lemmik", "status.favourite": "Lemmik",
"status.favourites": "{count, plural, one {lemmik} other {lemmikud}}",
"status.filter": "Filtreeri seda postitust", "status.filter": "Filtreeri seda postitust",
"status.filtered": "Filtreeritud", "status.filtered": "Filtreeritud",
"status.hide": "Peida postitus", "status.hide": "Peida postitus",
@ -675,6 +725,7 @@
"status.reblog": "Jaga", "status.reblog": "Jaga",
"status.reblog_private": "Jaga algse nähtavusega", "status.reblog_private": "Jaga algse nähtavusega",
"status.reblogged_by": "{name} jagas", "status.reblogged_by": "{name} jagas",
"status.reblogs": "{count, plural, one {jagamine} other {jagamist}}",
"status.reblogs.empty": "Keegi pole seda postitust veel jaganud. Kui keegi seda teeb, näeb seda siin.", "status.reblogs.empty": "Keegi pole seda postitust veel jaganud. Kui keegi seda teeb, näeb seda siin.",
"status.redraft": "Kustuta & alga uuesti", "status.redraft": "Kustuta & alga uuesti",
"status.remove_bookmark": "Eemalda järjehoidja", "status.remove_bookmark": "Eemalda järjehoidja",

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