Merge remote-tracking branch 'glitch/main'

main
Skye 2 years ago
commit e0f9ac242b
Signed by: me
GPG Key ID: 0104BC05F41B77B8

@ -1,128 +0,0 @@
version: 2.1
orbs:
ruby: circleci/ruby@2.0.0
node: circleci/node@5.0.3
executors:
default:
parameters:
ruby-version:
type: string
docker:
- image: cimg/ruby:<< parameters.ruby-version >>
environment:
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
CONTINUOUS_INTEGRATION: true
DB_HOST: localhost
DB_USER: root
DISABLE_SIMPLECOV: true
RAILS_ENV: test
- image: cimg/postgres:14.5
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: cimg/redis:7.0
commands:
install-system-dependencies:
steps:
- run:
name: Install system dependencies
command: |
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev
install-ruby-dependencies:
parameters:
ruby-version:
type: string
steps:
- run:
command: |
bundle config clean 'true'
bundle config frozen 'true'
bundle config without 'development production'
name: Set bundler settings
- ruby/install-deps:
bundler-version: '2.3.26'
key: ruby<< parameters.ruby-version >>-gems-v2
wait-db:
steps:
- run:
command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
name: Wait for PostgreSQL and Redis
jobs:
build:
docker:
- image: cimg/ruby:3.2-node
environment:
RAILS_ENV: test
steps:
- checkout
- install-system-dependencies
- install-ruby-dependencies:
ruby-version: '3.2'
- node/install-packages:
cache-version: v1
pkg-manager: yarn
- run:
command: |
export NODE_OPTIONS=--openssl-legacy-provider
./bin/rails assets:precompile
name: Precompile assets
- persist_to_workspace:
paths:
- public/assets
- public/packs-test
root: .
test:
parameters:
ruby-version:
type: string
executor:
name: default
ruby-version: << parameters.ruby-version >>
environment:
ALLOW_NOPAM: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
parallelism: 4
steps:
- checkout
- install-system-dependencies
- run:
command: sudo apt-get install -y ffmpeg imagemagick libmagickcore-dev libmagickwand-dev libjpeg-dev libpng-dev libtiff-dev libwebp-dev libpam-dev
name: Install additional system dependencies
- run:
command: bundle config with 'pam_authentication'
name: Enable PAM authentication
- install-ruby-dependencies:
ruby-version: << parameters.ruby-version >>
- attach_workspace:
at: .
- wait-db
- run:
command: ./bin/rails db:create db:schema:load db:seed
name: Load database schema
- ruby/rspec-test
workflows:
version: 2
build-and-test:
jobs:
- build
- test:
matrix:
parameters:
ruby-version:
- '2.7'
- '3.0'
- '3.1'
- '3.2'
name: test-ruby<< matrix.ruby-version >>
requires:
- build

@ -1,16 +1,14 @@
# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster # For details, see https://github.com/devcontainers/images/tree/main/src/ruby
ARG VARIANT=3.1-bullseye FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
# Install Rails # Install Rails
# RUN gem install rails webdrivers # RUN gem install rails webdrivers
# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service # Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
# The value is a comma-separated list of allowed domains # The value is a comma-separated list of allowed domains
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev" ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
# [Choice] Node.js version: lts/*, 18, 16, 14 ARG NODE_VERSION="16"
ARG NODE_VERSION="lts/*"
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
# [Optional] Uncomment this section to install additional OS packages. # [Optional] Uncomment this section to install additional OS packages.
@ -22,3 +20,5 @@ RUN gem install foreman
# [Optional] Uncomment this line to install global node packages. # [Optional] Uncomment this line to install global node packages.
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1 RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1
COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt

@ -1,30 +1,13 @@
// For more details, see https://aka.ms/devcontainer.json.
{ {
"name": "Mastodon", "name": "Mastodon",
"dockerComposeFile": "docker-compose.yml", "dockerComposeFile": "docker-compose.yml",
"service": "app", "service": "app",
"workspaceFolder": "/mastodon", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"EditorConfig.EditorConfig",
"dbaeumer.vscode-eslint",
"rebornix.Ruby",
"webben.browserslist"
]
}
},
// Features to add to the dev container. More info: https://containers.dev/features.
"features": { "features": {
"ghcr.io/devcontainers/features/sshd:1": { "ghcr.io/devcontainers/features/sshd:1": {}
"version": "latest"
}
}, },
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
@ -33,7 +16,16 @@
// Use 'postCreateCommand' to run commands after the container is created. // Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": ".devcontainer/post-create.sh", "postCreateCommand": ".devcontainer/post-create.sh",
"waitFor": "postCreateCommand",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. // Configure tool-specific properties.
"remoteUser": "vscode" "customizations": {
// Configure properties specific to VS Code.
"vscode": {
// Set *default* container specific settings.json values on container create.
"settings": {},
// Add the IDs of extensions you want installed when the container is created.
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
}
}
} }

@ -5,15 +5,8 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
args:
# Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6
# Append -bullseye or -buster to pin to an OS version.
# Use -bullseye variants on local arm64/Apple Silicon.
VARIANT: '3.0-bullseye'
# Optional Node.js version to install
NODE_VERSION: '16'
volumes: volumes:
- ..:/mastodon:cached - ../..:/workspaces:cached
environment: environment:
RAILS_ENV: development RAILS_ENV: development
NODE_ENV: development NODE_ENV: development
@ -33,7 +26,6 @@ services:
networks: networks:
- external_network - external_network
- internal_network - internal_network
user: vscode
db: db:
image: postgres:14-alpine image: postgres:14-alpine
@ -49,7 +41,7 @@ services:
- internal_network - internal_network
redis: redis:
image: redis:6-alpine image: redis:7-alpine
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- redis-data:/data - redis-data:/data

@ -0,0 +1,8 @@
👋 Welcome to "Mastodon" in GitHub Codespaces!
🛠️ Your environment is fully setup with all the required software.
🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1).
📝 Edit away, run your app as usual, and we'll automatically make it available for you to access.

@ -25,12 +25,15 @@ jobs:
- uses: hadolint/hadolint-action@v3.1.0 - uses: hadolint/hadolint-action@v3.1.0
- uses: docker/setup-qemu-action@v2 - uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2 - uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
- name: Log in to the Github Container registry
uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
- uses: docker/metadata-action@v4 - uses: docker/metadata-action@v4
id: meta id: meta
with: with:
@ -39,6 +42,7 @@ jobs:
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
type=edge,branch=main type=edge,branch=main
type=sha,prefix=,format=long type=sha,prefix=,format=long
- uses: docker/build-push-action@v4 - uses: docker/build-push-action@v4
with: with:
context: . context: .
@ -47,5 +51,6 @@ jobs:
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

@ -0,0 +1,17 @@
{
"problemMatcher": [
{
"owner": "haml-lint",
"severity": "warning",
"pattern": [
{
"regexp": "^(.*):(\\d+)\\s\\[W]\\s(.*):\\s(.*)$",
"file": 1,
"line": 2,
"code": 3,
"message": 4
}
]
}
]
}

@ -0,0 +1,44 @@
name: Haml Linting
on:
push:
branches-ignore:
- 'dependabot/**'
paths:
- '.github/workflows/haml-lint-problem-matcher.json'
- '.github/workflows/lint-haml.yml'
- '.haml-lint*.yml'
- '.rubocop*.yml'
- '.ruby-version'
- '**/*.haml'
- 'Gemfile*'
pull_request:
paths:
- '.github/workflows/haml-lint-problem-matcher.json'
- '.github/workflows/lint-haml.yml'
- '.haml-lint*.yml'
- '.rubocop*.yml'
- '.ruby-version'
- '**/*.haml'
- 'Gemfile*'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v3
- name: Install native Ruby dependencies
run: sudo apt-get install -y libicu-dev libidn11-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Run haml-lint
run: |
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
bundle exec haml-lint

@ -2,7 +2,13 @@ name: PR Needs Rebase
on: on:
push: push:
branches-ignore:
- 'dependabot/**'
- 'l10n_main'
pull_request_target: pull_request_target:
branches-ignore:
- 'dependabot/**'
- 'l10n_main'
types: [synchronize] types: [synchronize]
permissions: permissions:

@ -25,7 +25,7 @@ jobs:
services: services:
postgres: postgres:
image: postgres:14.5 image: postgres:14-alpine
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
@ -38,7 +38,7 @@ jobs:
- 5432:5432 - 5432:5432
redis: redis:
image: redis:7.0 image: redis:7-alpine
options: >- options: >-
--health-cmd "redis-cli ping" --health-cmd "redis-cli ping"
--health-interval 10s --health-interval 10s

@ -25,7 +25,7 @@ jobs:
services: services:
postgres: postgres:
image: postgres:14.5 image: postgres:14-alpine
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres
@ -37,7 +37,7 @@ jobs:
ports: ports:
- 5432:5432 - 5432:5432
redis: redis:
image: redis:7.0 image: redis:7-alpine
options: >- options: >-
--health-cmd "redis-cli ping" --health-cmd "redis-cli ping"
--health-interval 10s --health-interval 10s

@ -0,0 +1,141 @@
name: Ruby Testing
on:
push:
branches-ignore:
- 'dependabot/**'
pull_request:
env:
BUNDLE_CLEAN: true
BUNDLE_FROZEN: true
BUNDLE_WITHOUT: 'development production'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
env:
RAILS_ENV: test
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
cache: yarn
node-version-file: '.nvmrc'
- name: Install native Ruby dependencies
run: sudo apt-get install -y libicu-dev libidn11-dev
- name: Set up bundler cache
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- run: yarn install --frozen-lockfile
- name: Precompile assets
# Previously had set this, but it's not supported
# export NODE_OPTIONS=--openssl-legacy-provider
run: |-
./bin/rails assets:precompile
- uses: actions/upload-artifact@v3
with:
path: |-
./public/assets
./public/packs-test
name: ${{ github.sha }}
retention-days: 0
test:
runs-on: ubuntu-latest
needs:
- build
services:
postgres:
image: postgres:14-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379
env:
DB_HOST: localhost
DB_USER: postgres
DB_PASS: postgres
DISABLE_SIMPLECOV: true
RAILS_ENV: test
ALLOW_NOPAM: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
BUNDLE_WITH: 'pam_authentication'
CI_JOBS: ${{ matrix.ci_job }}/4
strategy:
fail-fast: false
matrix:
ruby-version:
- '2.7'
- '3.0'
- '3.1'
- '.ruby-version'
ci_job:
- 1
- 2
- 3
- 4
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
path: './public'
name: ${{ github.sha }}
- name: Install native Ruby dependencies
run: sudo apt-get install -y libicu-dev libidn11-dev
- name: Install additional system dependencies
run: sudo apt-get install -y ffmpeg imagemagick libpam-dev
- name: Set up bundler cache
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version}}
bundler-cache: true
- name: Update system gems
if: matrix.ruby-version == '2.7'
run: gem update --system
- name: Load database schema
run: './bin/rails db:create db:schema:load db:seed'
- run: bundle exec rake rspec_chunked

@ -1,108 +1,9 @@
# Whether to ignore frontmatter at the beginning of HAML documents for inherits_from: .haml-lint_todo.yml
# frameworks such as Jekyll/Middleman
skip_frontmatter: false
exclude: exclude:
- 'vendor/**/*' - 'vendor/**/*'
- 'spec/**/*' - lib/templates/haml/scaffold/_form.html.haml
- 'lib/templates/**/*'
- 'app/views/kaminari/**/*'
linters: linters:
AltText: AltText:
enabled: false
ClassAttributeWithStaticValue:
enabled: true
ClassesBeforeIds:
enabled: true
ConsecutiveComments:
enabled: true
ConsecutiveSilentScripts:
enabled: true
max_consecutive: 2
EmptyObjectReference:
enabled: true
EmptyScript:
enabled: true
FinalNewline:
enabled: true
present: true
HtmlAttributes:
enabled: true
ImplicitDiv:
enabled: true
LeadingCommentSpace:
enabled: true
LineLength:
enabled: false
max: 80
MultilinePipe:
enabled: true
MultilineScript:
enabled: true
ObjectReferenceAttributes:
enabled: true
RuboCop:
enabled: true
# These cops are incredibly noisy when it comes to HAML templates, so we
# ignore them.
ignored_cops:
- Lint/BlockAlignment
- Lint/EndAlignment
- Lint/Void
- Metrics/BlockLength
- Metrics/LineLength
- Style/AlignParameters
- Style/BlockNesting
- Style/ElseAlignment
- Style/EndOfLine
- Style/FileName
- Style/FinalNewline
- Style/FrozenStringLiteralComment
- Style/IfUnlessModifier
- Style/IndentationWidth
- Style/Next
- Style/TrailingBlankLines
- Style/TrailingWhitespace
- Style/WhileUntilModifier
RubyComments:
enabled: true
SpaceBeforeScript:
enabled: true
SpaceInsideHashAttributes:
enabled: true
style: space
Indentation:
enabled: true
character: space # or tab
TagName:
enabled: true
TrailingWhitespace:
enabled: true
UnnecessaryInterpolation:
enabled: true
UnnecessaryStringOutput:
enabled: true enabled: true

@ -0,0 +1,106 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
# on 2023-03-15 00:55:01 -0400 using Haml-Lint version 0.45.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
# Offense count: 63
RuboCop:
exclude:
- 'app/views/accounts/_og.html.haml'
- 'app/views/admin/account_warnings/_account_warning.html.haml'
- 'app/views/admin/accounts/index.html.haml'
- 'app/views/admin/accounts/show.html.haml'
- 'app/views/admin/announcements/edit.html.haml'
- 'app/views/admin/announcements/new.html.haml'
- 'app/views/admin/disputes/appeals/_appeal.html.haml'
- 'app/views/admin/domain_blocks/edit.html.haml'
- 'app/views/admin/domain_blocks/new.html.haml'
- 'app/views/admin/ip_blocks/new.html.haml'
- 'app/views/admin/reports/actions/preview.html.haml'
- 'app/views/admin/reports/index.html.haml'
- 'app/views/admin/reports/show.html.haml'
- 'app/views/admin/roles/_form.html.haml'
- 'app/views/admin/settings/about/show.html.haml'
- 'app/views/admin/settings/appearance/show.html.haml'
- 'app/views/admin/settings/registrations/show.html.haml'
- 'app/views/admin/statuses/show.html.haml'
- 'app/views/auth/registrations/new.html.haml'
- 'app/views/disputes/strikes/show.html.haml'
- 'app/views/filters/_filter_fields.html.haml'
- 'app/views/invites/_form.html.haml'
- 'app/views/layouts/application.html.haml'
- 'app/views/layouts/error.html.haml'
- 'app/views/notification_mailer/_status.html.haml'
- 'app/views/settings/applications/_fields.html.haml'
- 'app/views/settings/imports/show.html.haml'
- 'app/views/settings/preferences/appearance/show.html.haml'
- 'app/views/settings/preferences/other/show.html.haml'
- 'app/views/statuses/_detailed_status.html.haml'
- 'app/views/statuses/_poll.html.haml'
- 'app/views/statuses/show.html.haml'
- 'app/views/statuses_cleanup/show.html.haml'
- 'app/views/user_mailer/warning.html.haml'
# Offense count: 913
LineLength:
enabled: false
# Offense count: 22
UnnecessaryStringOutput:
exclude:
- 'app/views/accounts/show.html.haml'
- 'app/views/admin/custom_emojis/_custom_emoji.html.haml'
- 'app/views/admin/relays/_relay.html.haml'
- 'app/views/admin/rules/_rule.html.haml'
- 'app/views/admin/statuses/index.html.haml'
- 'app/views/auth/registrations/_sessions.html.haml'
- 'app/views/disputes/strikes/show.html.haml'
- 'app/views/notification_mailer/_status.html.haml'
- 'app/views/settings/two_factor_authentication_methods/index.html.haml'
- 'app/views/statuses/_detailed_status.html.haml'
- 'app/views/statuses/_poll.html.haml'
- 'app/views/statuses/_simple_status.html.haml'
- 'app/views/user_mailer/suspicious_sign_in.html.haml'
- 'app/views/user_mailer/webauthn_credential_added.html.haml'
- 'app/views/user_mailer/webauthn_credential_deleted.html.haml'
- 'app/views/user_mailer/welcome.html.haml'
# Offense count: 3
ViewLength:
exclude:
- 'app/views/admin/accounts/show.html.haml'
- 'app/views/admin/reports/show.html.haml'
- 'app/views/disputes/strikes/show.html.haml'
# Offense count: 41
InstanceVariables:
exclude:
- 'app/views/admin/reports/_actions.html.haml'
- 'app/views/admin/roles/_form.html.haml'
- 'app/views/admin/webhooks/_form.html.haml'
- 'app/views/auth/registrations/_sessions.html.haml'
- 'app/views/auth/registrations/_status.html.haml'
- 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml'
- 'app/views/authorize_interactions/_post_follow_actions.html.haml'
- 'app/views/invites/_form.html.haml'
- 'app/views/relationships/_account.html.haml'
- 'app/views/shared/_og.html.haml'
- 'app/views/statuses/_status.html.haml'
# Offense count: 6
ConsecutiveSilentScripts:
exclude:
- 'app/views/admin/settings/shared/_links.html.haml'
- 'app/views/settings/login_activities/_login_activity.html.haml'
- 'app/views/statuses/_poll.html.haml'
# Offense count: 3
IdNames:
exclude:
- 'app/views/authorize_interactions/error.html.haml'
- 'app/views/oauth/authorizations/error.html.haml'
- 'app/views/shared/_error_messages.html.haml'

@ -8,6 +8,7 @@ require:
- rubocop-rails - rubocop-rails
- rubocop-rspec - rubocop-rspec
- rubocop-performance - rubocop-performance
- rubocop-capybara
AllCops: AllCops:
TargetRubyVersion: 2.7 TargetRubyVersion: 2.7
@ -19,8 +20,6 @@ AllCops:
NewCops: enable NewCops: enable
Exclude: Exclude:
- db/schema.rb - db/schema.rb
- 'app/views/**/*'
- 'config/**/*'
- 'bin/*' - 'bin/*'
- 'Rakefile' - 'Rakefile'
- 'node_modules/**/*' - 'node_modules/**/*'
@ -33,7 +32,6 @@ Layout/FirstHashElementIndentation:
EnforcedStyle: consistent EnforcedStyle: consistent
Layout/LineLength: Layout/LineLength:
Max: 140 # RuboCop default 120
AllowedPatterns: AllowedPatterns:
# Allow comments to be long lines # Allow comments to be long lines
- !ruby/regexp / \# .*$/ - !ruby/regexp / \# .*$/
@ -48,13 +46,11 @@ Lint/UselessAccessModifier:
- class_methods - class_methods
Metrics/AbcSize: Metrics/AbcSize:
Max: 34 # RuboCop default 17
Exclude: Exclude:
- 'lib/**/*cli*.rb' - 'lib/**/*cli*.rb'
- db/*migrate/**/* - db/*migrate/**/*
Metrics/BlockLength: Metrics/BlockLength:
Max: 55 # Default 25
CountAsOne: [array, heredoc] CountAsOne: [array, heredoc]
Exclude: Exclude:
- 'lib/mastodon/*_cli.rb' - 'lib/mastodon/*_cli.rb'
@ -64,30 +60,23 @@ Metrics/BlockNesting:
- 'lib/mastodon/*_cli.rb' - 'lib/mastodon/*_cli.rb'
Metrics/ClassLength: Metrics/ClassLength:
Max: 500 # Default 100
CountAsOne: [array, heredoc] CountAsOne: [array, heredoc]
Exclude: Exclude:
- 'lib/mastodon/*_cli.rb' - 'lib/mastodon/*_cli.rb'
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 12 # Default 7
Exclude: Exclude:
- lib/mastodon/*cli*.rb - lib/mastodon/*cli*.rb
- db/*migrate/**/* - db/*migrate/**/*
Metrics/MethodLength: Metrics/MethodLength:
Max: 25 # RuboCop default 10
CountAsOne: [array, heredoc] CountAsOne: [array, heredoc]
Exclude: Exclude:
- 'lib/mastodon/*_cli.rb' - 'lib/mastodon/*_cli.rb'
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 200 # Default 100
CountAsOne: [array, heredoc] CountAsOne: [array, heredoc]
Metrics/PerceivedComplexity:
Max: 16 # RuboCop default 8
Rails/HttpStatus: Rails/HttpStatus:
EnforcedStyle: numeric EnforcedStyle: numeric
@ -97,12 +86,43 @@ Rails/Exit:
- 'lib/mastodon/cli_helper.rb' - 'lib/mastodon/cli_helper.rb'
- 'lib/cli.rb' - 'lib/cli.rb'
# Reason: Some single letter camel case files shouldn't be split
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath
RSpec/FilePath:
CustomTransform:
ActivityPub: activitypub # Ignore the snake_case due to the amount of files to rename
DeepL: deepl
FetchOEmbedService: fetch_oembed_service
JsonLdHelper: jsonld_helper
OEmbedController: oembed_controller
OStatus: ostatus
NodeInfoController: nodeinfo_controller # NodeInfo isn't snake_cased for any of the instances
Exclude:
- 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder
- 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder
- 'spec/controllers/concerns/account_controller_concern_spec.rb' # Concerns describe ApplicationController and don't fit naming
- 'spec/controllers/concerns/export_controller_concern_spec.rb'
- 'spec/controllers/concerns/localized_spec.rb'
- 'spec/controllers/concerns/rate_limit_headers_spec.rb'
- 'spec/controllers/concerns/signature_verification_spec.rb'
- 'spec/controllers/concerns/user_tracking_concern_spec.rb'
RSpec/NotToNot: RSpec/NotToNot:
EnforcedStyle: to_not EnforcedStyle: to_not
RSpec/Rails/HttpStatus: RSpec/Rails/HttpStatus:
EnforcedStyle: numeric EnforcedStyle: numeric
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren
Style/ClassAndModuleChildren:
Enabled: false
# Reason: Classes mostly self-document with their names
# https://docs.rubocop.org/rubocop/cops_style.html#styledocumentation
Style/Documentation:
Enabled: false
Style/HashSyntax: Style/HashSyntax:
EnforcedStyle: ruby19_no_mixed_keys EnforcedStyle: ruby19_no_mixed_keys
@ -123,3 +143,6 @@ Style/TrailingCommaInArrayLiteral:
Style/TrailingCommaInHashLiteral: Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma' EnforcedStyleForMultiline: 'comma'
Style/SymbolArray:
Enabled: false

File diff suppressed because it is too large Load Diff

@ -2,6 +2,57 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.1.1] - 2023-03-16
### Added
- Add redirection from paths with url-encoded `@` to their decoded form ([thijskh](https://github.com/mastodon/mastodon/pull/23593))
- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749))
- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597))
- Add support for refreshing many accounts at once with `tootctl accounts refresh` ([9p4](https://github.com/mastodon/mastodon/pull/23304))
- Add confirmation modal when clicking to edit a post with a non-empty compose form ([PauloVilarinho](https://github.com/mastodon/mastodon/pull/23936))
- Add support for the HAproxy PROXY protocol through the `PROXY_PROTO_V1` environment variable ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24064))
- Add `SENDFILE_HEADER` environment variable ([Gargron](https://github.com/mastodon/mastodon/pull/24123))
- Add cache headers to static files served through Rails ([Gargron](https://github.com/mastodon/mastodon/pull/24120))
### Changed
- Increase contrast of upload progress bar background ([toolmantim](https://github.com/mastodon/mastodon/pull/23836))
- Change post auto-deletion throttling constants to better scale with server size ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23320))
- Change order of bookmark and favourite sidebar entries in single-column UI for consistency ([TerryGarcia](https://github.com/mastodon/mastodon/pull/23701))
- Change `ActivityPub::DeliveryWorker` retries to be spread out more ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21956))
### Fixed
- Fix “Remove all followers from the selected domains” also removing follows and notifications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805))
- Fix streaming metrics format ([emilweth](https://github.com/mastodon/mastodon/pull/23519), [emilweth](https://github.com/mastodon/mastodon/pull/23520))
- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526))
- Fix focus point of already-attached media not saving after edit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23566))
- Fix sidebar behavior in settings/admin UI on mobile ([wxt2005](https://github.com/mastodon/mastodon/pull/23764))
- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801))
- Fix duplicate “Publish” button on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23804))
- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787))
- Fix server error when attempting to display the edit history of a trendable post in the admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23574))
- Fix `tootctl accounts migrate` crashing because of a typo ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23567))
- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957))
- Fix the “Back” button in column headers sometimes leaving Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953))
- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958))
- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803))
- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988))
- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029))
- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046))
- Fix tags being unnecessarily stripped from plain-text short site description ([c960657](https://github.com/mastodon/mastodon/pull/23975))
- Fix HTML entities not being un-escaped in extracted plain-text from remote posts ([c960657](https://github.com/mastodon/mastodon/pull/24019))
- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751))
- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611))
- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568))
- Fix duplicate mails being sent when the SMTP server is too slow to close the connection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23750))
### Security
- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136))
- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137))
## [4.1.0] - 2023-02-10 ## [4.1.0] - 2023-02-10
### Added ### Added

@ -80,8 +80,6 @@ It is not always possible to phrase every change in such a manner, but it is des
- Code style rules (rubocop, eslint) - Code style rules (rubocop, eslint)
- Normalization of locale files (i18n-tasks) - Normalization of locale files (i18n-tasks)
**Note**: You may need to log in and authorise the GitHub account your fork of this repository belongs to with CircleCI to enable some of the automated checks to run.
## Documentation ## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).

@ -4,9 +4,8 @@ source 'https://rubygems.org'
ruby '>= 2.7.0', '< 3.3.0' ruby '>= 2.7.0', '< 3.3.0'
gem 'pkg-config', '~> 1.5' gem 'pkg-config', '~> 1.5'
gem 'rexml', '~> 3.2'
gem 'puma', '~> 5.6' gem 'puma', '~> 6.1'
gem 'rails', '~> 6.1.7' gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2' gem 'thor', '~> 1.2'
@ -40,7 +39,7 @@ end
gem 'net-ldap', '~> 0.17' gem 'net-ldap', '~> 0.17'
gem 'omniauth-cas', '~> 2.0' gem 'omniauth-cas', '~> 2.0'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth_openid_connect', '~> 0.6.0' gem 'omniauth_openid_connect', '~> 0.6.1'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 1.9'
gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'omniauth-rails_csrf_protection', '~> 0.1'
@ -70,7 +69,7 @@ gem 'public_suffix', '~> 5.0'
gem 'pundit', '~> 2.3' gem 'pundit', '~> 2.3'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.6' gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 1.1', require: 'rack/cors' gem 'rack-cors', '~> 2.0', require: 'rack/cors'
gem 'rails-i18n', '~> 6.0' gem 'rails-i18n', '~> 6.0'
gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true'
gem 'redcarpet', '~> 3.6' gem 'redcarpet', '~> 3.6'
@ -104,9 +103,10 @@ group :development, :test do
gem 'fabrication', '~> 2.30' gem 'fabrication', '~> 2.30'
gem 'fuubar', '~> 2.5' gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 1.0', require: false gem 'i18n-tasks', '~> 1.0', require: false
gem 'pry-byebug', '~> 3.10' gem 'rspec-rails', '~> 6.0'
gem 'pry-rails', '~> 0.3' gem 'rspec_chunked', '~> 0.6'
gem 'rspec-rails', '~> 5.1'
gem 'rubocop-capybara', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false gem 'rubocop-rspec', require: false
@ -119,10 +119,10 @@ end
group :test do group :test do
gem 'capybara', '~> 3.38' gem 'capybara', '~> 3.38'
gem 'climate_control', '~> 0.2' gem 'climate_control'
gem 'faker', '~> 3.1' gem 'faker', '~> 3.1'
gem 'json-schema', '~> 3.0' gem 'json-schema', '~> 3.0'
gem 'rack-test', '~> 2.0' gem 'rack-test', '~> 2.1'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec_junit_formatter', '~> 0.6' gem 'rspec_junit_formatter', '~> 0.6'
gem 'rspec-sidekiq', '~> 3.1' gem 'rspec-sidekiq', '~> 3.1'
@ -131,16 +131,15 @@ group :test do
end end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.8'
gem 'annotate', '~> 3.2' gem 'annotate', '~> 3.2'
gem 'better_errors', '~> 2.9' gem 'better_errors', '~> 2.9'
gem 'binding_of_caller', '~> 1.0' gem 'binding_of_caller', '~> 1.0'
gem 'bullet', '~> 7.0'
gem 'letter_opener', '~> 1.8' gem 'letter_opener', '~> 1.8'
gem 'letter_opener_web', '~> 2.0' gem 'letter_opener_web', '~> 2.0'
gem 'memory_profiler' gem 'memory_profiler'
gem 'brakeman', '~> 5.4', require: false gem 'brakeman', '~> 5.4', require: false
gem 'bundler-audit', '~> 0.9', require: false gem 'bundler-audit', '~> 0.9', require: false
gem 'haml_lint', require: false
gem 'capistrano', '~> 3.17' gem 'capistrano', '~> 3.17'
gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rails', '~> 1.6'

@ -30,40 +30,40 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.7.2) actioncable (6.1.7.3)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.2) actionmailbox (6.1.7.3)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.3)
activejob (= 6.1.7.2) activejob (= 6.1.7.3)
activerecord (= 6.1.7.2) activerecord (= 6.1.7.3)
activestorage (= 6.1.7.2) activestorage (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.7.2) actionmailer (6.1.7.3)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.3)
actionview (= 6.1.7.2) actionview (= 6.1.7.3)
activejob (= 6.1.7.2) activejob (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.7.2) actionpack (6.1.7.3)
actionview (= 6.1.7.2) actionview (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.7.2) actiontext (6.1.7.3)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.3)
activerecord (= 6.1.7.2) activerecord (= 6.1.7.3)
activestorage (= 6.1.7.2) activestorage (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.7.2) actionview (6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -73,23 +73,22 @@ GEM
activemodel (>= 4.1, < 7.1) activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8) activejob (6.1.7.3)
activejob (6.1.7.2) activesupport (= 6.1.7.3)
activesupport (= 6.1.7.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.7.2) activemodel (6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
activerecord (6.1.7.2) activerecord (6.1.7.3)
activemodel (= 6.1.7.2) activemodel (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
activestorage (6.1.7.2) activestorage (6.1.7.3)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.3)
activejob (= 6.1.7.2) activejob (= 6.1.7.3)
activerecord (= 6.1.7.2) activerecord (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.7.2) activesupport (6.1.7.3)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -144,18 +143,14 @@ GEM
bootsnap (1.16.0) bootsnap (1.16.0)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (5.4.0) brakeman (5.4.0)
browser (4.2.0) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, < 6) redis (>= 1.0, < 6)
builder (3.2.4) builder (3.2.4)
bullet (7.0.7)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.9.1) bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
byebug (11.1.3)
capistrano (3.17.2) capistrano (3.17.2)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
@ -193,7 +188,7 @@ GEM
cocoon (1.2.15) cocoon (1.2.15)
coderay (1.1.3) coderay (1.1.3)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.2.0) concurrent-ruby (1.2.2)
connection_pool (2.3.0) connection_pool (2.3.0)
cose (1.3.0) cose (1.3.0)
cbor (~> 0.5.9) cbor (~> 0.5.9)
@ -226,7 +221,7 @@ GEM
docile (1.4.0) docile (1.4.0)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.6.4) doorkeeper (5.6.5)
railties (>= 5) railties (>= 5)
dotenv (2.8.1) dotenv (2.8.1)
dotenv-rails (2.8.1) dotenv-rails (2.8.1)
@ -309,6 +304,12 @@ GEM
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.45.0)
haml (>= 4.0, < 6.2)
parallel (~> 1.10)
rainbow
rubocop (>= 0.50.0)
sysexits (~> 1.1)
hashdiff (1.0.1) hashdiff (1.0.1)
hashie (5.0.0) hashie (5.0.0)
hcaptcha (7.1.0) hcaptcha (7.1.0)
@ -400,7 +401,7 @@ GEM
loofah (2.19.1) loofah (2.19.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.8.0.1) mail (2.8.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap net-imap
net-pop net-pop
@ -418,7 +419,7 @@ GEM
mime-types-data (3.2022.0105) mime-types-data (3.2022.0105)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.1) mini_portile2 (2.8.1)
minitest (5.17.0) minitest (5.18.0)
msgpack (1.6.0) msgpack (1.6.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
@ -460,7 +461,7 @@ GEM
omniauth-saml (1.10.3) omniauth-saml (1.10.3)
omniauth (~> 1.3, >= 1.3.2) omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.9) ruby-saml (~> 1.9)
omniauth_openid_connect (0.6.0) omniauth_openid_connect (0.6.1)
omniauth (>= 1.9, < 3) omniauth (>= 1.9, < 3)
openid_connect (~> 1.1) openid_connect (~> 1.1)
openid_connect (1.4.2) openid_connect (1.4.2)
@ -480,13 +481,13 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.14.14) ox (2.14.14)
parallel (1.22.1) parallel (1.22.1)
parser (3.2.1.0) parser (3.2.1.1)
ast (~> 2.4.1) ast (~> 2.4.1)
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.4.5) pg (1.4.6)
pghero (3.1.0) pghero (3.3.1)
activerecord (>= 6) activerecord (>= 6)
pkg-config (1.5.1) pkg-config (1.5.1)
posix-spawn (0.3.15) posix-spawn (0.3.15)
@ -499,25 +500,17 @@ GEM
net-smtp net-smtp
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0) private_address_check (0.5.0)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.10.1)
byebug (~> 11.0)
pry (>= 0.13, < 0.15)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1) public_suffix (5.0.1)
puma (5.6.5) puma (6.1.1)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.2) racc (1.6.2)
rack (2.2.6.2) rack (2.2.6.4)
rack-attack (6.6.1) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-oauth2 (1.21.3) rack-oauth2 (1.21.3)
activesupport activesupport
@ -527,22 +520,22 @@ GEM
rack (>= 2.1.0) rack (>= 2.1.0)
rack-proxy (0.7.6) rack-proxy (0.7.6)
rack rack
rack-test (2.0.2) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rails (6.1.7.2) rails (6.1.7.3)
actioncable (= 6.1.7.2) actioncable (= 6.1.7.3)
actionmailbox (= 6.1.7.2) actionmailbox (= 6.1.7.3)
actionmailer (= 6.1.7.2) actionmailer (= 6.1.7.3)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.3)
actiontext (= 6.1.7.2) actiontext (= 6.1.7.3)
actionview (= 6.1.7.2) actionview (= 6.1.7.3)
activejob (= 6.1.7.2) activejob (= 6.1.7.3)
activemodel (= 6.1.7.2) activemodel (= 6.1.7.3)
activerecord (= 6.1.7.2) activerecord (= 6.1.7.3)
activestorage (= 6.1.7.2) activestorage (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.7.2) railties (= 6.1.7.3)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
@ -556,9 +549,9 @@ GEM
rails-i18n (6.0.0) rails-i18n (6.0.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
railties (6.1.7.2) railties (6.1.7.3)
actionpack (= 6.1.7.2) actionpack (= 6.1.7.3)
activesupport (= 6.1.7.2) activesupport (= 6.1.7.3)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@ -569,7 +562,7 @@ GEM
rdf-normalize (0.5.1) rdf-normalize (0.5.1)
rdf (~> 3.2) rdf (~> 3.2)
redcarpet (3.6.0) redcarpet (3.6.0)
redis (4.5.1) redis (4.8.1)
redis-namespace (1.10.0) redis-namespace (1.10.0)
redis (>= 4) redis (>= 4)
redlock (1.3.2) redlock (1.3.2)
@ -587,53 +580,54 @@ GEM
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.11.0) rspec-core (3.12.1)
rspec-support (~> 3.11.0) rspec-support (~> 3.12.0)
rspec-expectations (3.11.0) rspec-expectations (3.12.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0) rspec-support (~> 3.12.0)
rspec-mocks (3.11.1) rspec-mocks (3.12.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0) rspec-support (~> 3.12.0)
rspec-rails (5.1.2) rspec-rails (6.0.1)
actionpack (>= 5.2) actionpack (>= 6.1)
activesupport (>= 5.2) activesupport (>= 6.1)
railties (>= 5.2) railties (>= 6.1)
rspec-core (~> 3.10) rspec-core (~> 3.11)
rspec-expectations (~> 3.10) rspec-expectations (~> 3.11)
rspec-mocks (~> 3.10) rspec-mocks (~> 3.11)
rspec-support (~> 3.10) rspec-support (~> 3.11)
rspec-sidekiq (3.1.0) rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.11.1) rspec-support (3.12.0)
rspec_chunked (0.6)
rspec_junit_formatter (0.6.0) rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.45.1) rubocop (1.48.1)
json (~> 2.3) json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.0.0) parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.24.1, < 2.0) rubocop-ast (>= 1.26.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.24.1) rubocop-ast (1.27.0)
parser (>= 3.1.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.17.0) rubocop-capybara (2.17.1)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-performance (1.16.0) rubocop-performance (1.16.0)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.17.4) rubocop-rails (2.18.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.18.1) rubocop-rspec (2.19.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
ruby-progressbar (1.11.0) ruby-progressbar (1.13.0)
ruby-saml (1.13.0) ruby-saml (1.13.0)
nokogiri (>= 1.10.5) nokogiri (>= 1.10.5)
rexml rexml
@ -655,9 +649,9 @@ GEM
redis (>= 4.5.0, < 5) redis (>= 4.5.0, < 5)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
sidekiq sidekiq
sidekiq-scheduler (5.0.1) sidekiq-scheduler (5.0.2)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 4, < 8) sidekiq (>= 6, < 8)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.29) sidekiq-unique-jobs (7.1.29)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0) brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
@ -687,7 +681,7 @@ GEM
sshkit (1.21.4) sshkit (1.21.4)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.23) stackprof (0.2.24)
statsd-ruby (1.5.0) statsd-ruby (1.5.0)
stoplight (3.0.1) stoplight (3.0.1)
redlock (~> 1.0) redlock (~> 1.0)
@ -697,14 +691,15 @@ GEM
activesupport (>= 3) activesupport (>= 3)
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
httpclient (>= 2.4) httpclient (>= 2.4)
sysexits (1.2.0)
temple (0.10.0) temple (0.10.0)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (1.2.1) thor (1.2.1)
tilt (2.0.11) tilt (2.1.0)
timeout (0.3.1) timeout (0.3.2)
tpm-key_attestation (0.12.0) tpm-key_attestation (0.12.0)
bindata (~> 2.4) bindata (~> 2.4)
openssl (> 2.0) openssl (> 2.0)
@ -730,7 +725,6 @@ GEM
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.8.2)
unicode-display_width (2.4.2) unicode-display_width (2.4.2)
uniform_notifier (1.16.0)
uri (0.12.0) uri (0.12.0)
validate_email (0.1.6) validate_email (0.1.6)
activemodel (>= 3.0) activemodel (>= 3.0)
@ -775,7 +769,6 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
active_model_serializers (~> 0.10) active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.8)
addressable (~> 2.8) addressable (~> 2.8)
annotate (~> 3.2) annotate (~> 3.2)
aws-sdk-s3 (~> 1.119) aws-sdk-s3 (~> 1.119)
@ -785,7 +778,6 @@ DEPENDENCIES
bootsnap (~> 1.16.0) bootsnap (~> 1.16.0)
brakeman (~> 5.4) brakeman (~> 5.4)
browser browser
bullet (~> 7.0)
bundler-audit (~> 0.9) bundler-audit (~> 0.9)
capistrano (~> 3.17) capistrano (~> 3.17)
capistrano-rails (~> 1.6) capistrano-rails (~> 1.6)
@ -794,7 +786,7 @@ DEPENDENCIES
capybara (~> 3.38) capybara (~> 3.38)
charlock_holmes (~> 0.7.7) charlock_holmes (~> 0.7.7)
chewy (~> 7.2) chewy (~> 7.2)
climate_control (~> 0.2) climate_control
cocoon (~> 1.2) cocoon (~> 1.2)
color_diff (~> 0.1) color_diff (~> 0.1)
concurrent-ruby concurrent-ruby
@ -814,6 +806,7 @@ DEPENDENCIES
fog-openstack (~> 0.3) fog-openstack (~> 0.3)
fuubar (~> 2.5) fuubar (~> 2.5)
haml-rails (~> 2.0) haml-rails (~> 2.0)
haml_lint
hcaptcha (~> 7.1) hcaptcha (~> 7.1)
hiredis (~> 0.6) hiredis (~> 0.6)
htmlentities (~> 4.3) htmlentities (~> 4.3)
@ -844,7 +837,7 @@ DEPENDENCIES
omniauth-cas (~> 2.0) omniauth-cas (~> 2.0)
omniauth-rails_csrf_protection (~> 0.1) omniauth-rails_csrf_protection (~> 0.1)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
omniauth_openid_connect (~> 0.6.0) omniauth_openid_connect (~> 0.6.1)
ox (~> 2.14) ox (~> 2.14)
parslet parslet
pg (~> 1.4) pg (~> 1.4)
@ -853,15 +846,13 @@ DEPENDENCIES
posix-spawn posix-spawn
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
pry-byebug (~> 3.10)
pry-rails (~> 0.3)
public_suffix (~> 5.0) public_suffix (~> 5.0)
puma (~> 5.6) puma (~> 6.1)
pundit (~> 2.3) pundit (~> 2.3)
rack (~> 2.2.6) rack (~> 2.2.6)
rack-attack (~> 6.6) rack-attack (~> 6.6)
rack-cors (~> 1.1) rack-cors (~> 2.0)
rack-test (~> 2.0) rack-test (~> 2.1)
rails (~> 6.1.7) rails (~> 6.1.7)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 6.0) rails-i18n (~> 6.0)
@ -870,12 +861,13 @@ DEPENDENCIES
redcarpet (~> 3.6) redcarpet (~> 3.6)
redis (~> 4.5) redis (~> 4.5)
redis-namespace (~> 1.10) redis-namespace (~> 1.10)
rexml (~> 3.2)
rqrcode (~> 2.1) rqrcode (~> 2.1)
rspec-rails (~> 5.1) rspec-rails (~> 6.0)
rspec-sidekiq (~> 3.1) rspec-sidekiq (~> 3.1)
rspec_chunked (~> 0.6)
rspec_junit_formatter (~> 0.6) rspec_junit_formatter (~> 0.6)
rubocop rubocop
rubocop-capybara
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec

@ -2,7 +2,7 @@
module Admin module Admin
class DomainBlocksController < BaseController class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:show, :destroy, :edit, :update] before_action :set_domain_block, only: [:destroy, :edit, :update]
def batch def batch
authorize :domain_block, :create? authorize :domain_block, :create?

@ -2,8 +2,6 @@
module Admin module Admin
class EmailDomainBlocksController < BaseController class EmailDomainBlocksController < BaseController
before_action :set_email_domain_block, only: [:show, :destroy]
def index def index
authorize :email_domain_block, :index? authorize :email_domain_block, :index?
@ -59,10 +57,6 @@ module Admin
private private
def set_email_domain_block
@email_domain_block = EmailDomainBlock.find(params[:id])
end
def set_resolved_records def set_resolved_records
Resolv::DNS.open do |dns| Resolv::DNS.open do |dns|
dns.timeouts = 5 dns.timeouts = 5

@ -0,0 +1,23 @@
# frozen_string_literal: true
class Api::V1::Instances::TranslationLanguagesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
before_action :set_languages
def show
expires_in 1.day, public: true
render json: @languages
end
private
def set_languages
if TranslationService.configured?
@languages = Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { TranslationService.configured.languages }
@languages['und'] = @languages.delete(nil) if @languages.key?(nil)
else
@languages = {}
end
end
end

@ -18,6 +18,8 @@ class ApplicationController < ActionController::Base
helper_method :current_skin helper_method :current_skin
helper_method :single_user_mode? helper_method :single_user_mode?
helper_method :use_seamless_external_login? helper_method :use_seamless_external_login?
helper_method :omniauth_only?
helper_method :sso_account_settings
helper_method :whitelist_mode? helper_method :whitelist_mode?
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
@ -63,8 +65,12 @@ class ApplicationController < ActionController::Base
end end
def after_sign_out_path_for(_resource_or_scope) def after_sign_out_path_for(_resource_or_scope)
if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true'
'/auth/auth/openid_connect/logout'
else
new_user_session_path new_user_session_path
end end
end
protected protected
@ -116,6 +122,14 @@ class ApplicationController < ActionController::Base
Devise.pam_authentication || Devise.ldap_authentication Devise.pam_authentication || Devise.ldap_authentication
end end
def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end
def sso_account_settings
ENV.fetch('SSO_ACCOUNT_SETTINGS')
end
def current_account def current_account
return @current_account if defined?(@current_account) return @current_account if defined?(@current_account)

@ -33,7 +33,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
if resource.email_present? if resource.email_present?
root_path stored_location_for(resource) || root_path
else else
auth_setup_path(missing_email: '1') auth_setup_path(missing_email: '1')
end end

@ -0,0 +1,27 @@
# frozen_string_literal: true
class BackupsController < ApplicationController
include RoutingHelper
skip_before_action :require_functional!
before_action :authenticate_user!
before_action :set_backup
def download
case Paperclip::Attachment.default_options[:storage]
when :s3
redirect_to @backup.dump.expiring_url(10)
when :fog
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
when :filesystem
redirect_to full_asset_url(@backup.dump.url)
end
end
private
def set_backup
@backup = current_user.backups.find(params[:id])
end
end

@ -3,6 +3,158 @@
module CacheConcern module CacheConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
module ActiveRecordCoder
EMPTY_HASH = {}.freeze
class << self
def dump(record)
instances = InstanceTracker.new
serialized_associations = serialize_associations(record, instances)
serialized_records = instances.map { |r| serialize_record(r) }
[serialized_associations, *serialized_records]
end
def load(payload)
instances = InstanceTracker.new
serialized_associations, *serialized_records = payload
serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) }
deserialize_associations(serialized_associations, instances)
end
private
# Records without associations, or which have already been visited before,
# are serialized by their id alone.
#
# Records with associations are serialized as a two-element array including
# their id and the record's association cache.
#
def serialize_associations(record, instances)
return unless record
if (id = instances.lookup(record))
payload = id
else
payload = instances.push(record)
cached_associations = record.class.reflect_on_all_associations.select do |reflection|
record.association_cached?(reflection.name)
end
unless cached_associations.empty?
serialized_associations = cached_associations.map do |reflection|
association = record.association(reflection.name)
serialized_target = if reflection.collection?
association.target.map { |target_record| serialize_associations(target_record, instances) }
else
serialize_associations(association.target, instances)
end
[reflection.name, serialized_target]
end
payload = [payload, serialized_associations]
end
end
payload
end
def deserialize_associations(payload, instances)
return unless payload
id, associations = payload
record = instances.fetch(id)
associations&.each do |name, serialized_target|
begin
association = record.association(name)
rescue ActiveRecord::AssociationNotFoundError
raise AssociationMissingError, "undefined association: #{name}"
end
target = if association.reflection.collection?
serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) }
else
deserialize_associations(serialized_target, instances)
end
association.target = target
end
record
end
def serialize_record(record)
arguments = [record.class.name, attributes_for_database(record)]
arguments << true if record.new_record?
arguments
end
if Rails.gem_version >= Gem::Version.new('7.0')
def attributes_for_database(record)
attributes = record.attributes_for_database
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
attributes
end
else
def attributes_for_database(record)
attributes = record.instance_variable_get(:@attributes).send(:attributes).transform_values(&:value_for_database)
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
attributes
end
end
def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter
begin
klass = Object.const_get(class_name)
rescue NameError
raise ClassMissingError, "undefined class: #{class_name}"
end
# Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass
# wether the record was persisted or not.
attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH)
klass.allocate.init_with_attributes(attributes, new_record)
end
end
class Error < StandardError
end
class ClassMissingError < Error
end
class AssociationMissingError < Error
end
class InstanceTracker
def initialize
@instances = []
@ids = {}.compare_by_identity
end
def map(&block)
@instances.map(&block)
end
def fetch(...)
@instances.fetch(...)
end
def push(instance)
id = @ids[instance] = @instances.size
@instances << instance
id
end
def lookup(instance)
@ids[instance]
end
end
end
def render_with_cache(**options) def render_with_cache(**options)
raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given? raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given?
@ -34,7 +186,12 @@ module CacheConcern
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
return [] if raw.empty? return [] if raw.empty?
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) cached_keys_with_value = begin
Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) }
rescue ActiveRecordCoder::Error
{} # The serialization format may have changed, let's pretend it's a cache miss.
end
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
@ -43,7 +200,7 @@ module CacheConcern
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
uncached.each_value do |item| uncached.each_value do |item|
Rails.cache.write(item, item) Rails.cache.write(item, ActiveRecordCoder.dump(item))
end end
end end

@ -138,7 +138,7 @@ module SignatureVerification
end end
def signed_headers def signed_headers
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split(' ') signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split
end end
def verify_signature_strength! def verify_signature_strength!

@ -20,6 +20,8 @@ class RelationshipsController < ApplicationController
@form.save @form.save
rescue ActionController::ParameterMissing rescue ActionController::ParameterMissing
# Do nothing # Do nothing
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound
flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow'
ensure ensure
redirect_to relationships_path(filter_params) redirect_to relationships_path(filter_params)
end end
@ -61,8 +63,8 @@ class RelationshipsController < ApplicationController
'unfollow' 'unfollow'
elsif params[:remove_from_followers] elsif params[:remove_from_followers]
'remove_from_followers' 'remove_from_followers'
elsif params[:block_domains] elsif params[:block_domains] || params[:remove_domains_from_followers]
'block_domains' 'remove_domains_from_followers'
end end
end end

@ -52,7 +52,7 @@ module Settings
end end
else else
flash[:error] = I18n.t('webauthn_credentials.create.error') flash[:error] = I18n.t('webauthn_credentials.create.error')
status = :internal_server_error status = :unprocessable_entity
end end
else else
flash[:error] = t('webauthn_credentials.create.error') flash[:error] = t('webauthn_credentials.create.error')

@ -1,11 +0,0 @@
# frozen_string_literal: true
module Admin::AnnouncementsHelper
def time_range(announcement)
if announcement.all_day?
safe_join([l(announcement.starts_at.to_date), ' - ', l(announcement.ends_at.to_date)])
else
safe_join([l(announcement.starts_at), ' - ', l(announcement.ends_at)])
end
end
end

@ -112,7 +112,7 @@ module ApplicationHelper
def fa_icon(icon, attributes = {}) def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || [] class_names = attributes[:class]&.split(' ') || []
class_names << 'fa' class_names << 'fa'
class_names += icon.split(' ').map { |cl| "fa-#{cl}" } class_names += icon.split.map { |cl| "fa-#{cl}" }
content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end end
@ -164,7 +164,7 @@ module ApplicationHelper
end end
def body_classes def body_classes
output = (@body_classes || '').split(' ') output = (@body_classes || '').split
output << "flavour-#{current_flavour.parameterize}" output << "flavour-#{current_flavour.parameterize}"
output << "skin-#{current_skin.parameterize}" output << "skin-#{current_skin.parameterize}"
output << 'system-font' if current_account&.user&.setting_system_font_ui output << 'system-font' if current_account&.user&.setting_system_font_ui

@ -41,9 +41,9 @@ module HomeHelper
def obscured_counter(count) def obscured_counter(count)
if count <= 0 if count <= 0
0 '0'
elsif count == 1 elsif count == 1
1 '1'
else else
'1+' '1+'
end end
@ -57,14 +57,6 @@ module HomeHelper
end end
end end
def optional_link_to(condition, path, options = {}, &block)
if condition
link_to(path, options, &block)
else
content_tag(:div, &block)
end
end
def sign_up_message def sign_up_message
if closed_registrations? if closed_registrations?
t('auth.registration_closed', instance: site_hostname) t('auth.registration_closed', instance: site_hostname)

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
# rubocop:disable Metrics/ModuleLength, Style/WordArray # rubocop:disable Metrics/ModuleLength
module LanguagesHelper module LanguagesHelper
ISO_639_1 = { ISO_639_1 = {
@ -275,4 +275,4 @@ module LanguagesHelper
end end
end end
# rubocop:enable Metrics/ModuleLength, Style/WordArray # rubocop:enable Metrics/ModuleLength

@ -5,6 +5,10 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL';
export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
@ -37,6 +41,29 @@ const fetchServerFail = error => ({
error, error,
}); });
export const fetchServerTranslationLanguages = () => (dispatch, getState) => {
dispatch(fetchServerTranslationLanguagesRequest());
api(getState)
.get('/api/v1/instance/translation_languages').then(({ data }) => {
dispatch(fetchServerTranslationLanguagesSuccess(data));
}).catch(err => dispatch(fetchServerTranslationLanguagesFail(err)));
};
const fetchServerTranslationLanguagesRequest = () => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
});
const fetchServerTranslationLanguagesSuccess = translationLanguages => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
translationLanguages,
});
const fetchServerTranslationLanguagesFail = error => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
error,
});
export const fetchExtendedDescription = () => (dispatch, getState) => { export const fetchExtendedDescription = () => (dispatch, getState) => {
dispatch(fetchExtendedDescriptionRequest()); dispatch(fetchExtendedDescriptionRequest());

@ -6,6 +6,7 @@ export default class GIFV extends React.PureComponent {
static propTypes = { static propTypes = {
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
onClick: PropTypes.func, onClick: PropTypes.func,
@ -35,7 +36,7 @@ export default class GIFV extends React.PureComponent {
}; };
render () { render () {
const { src, width, height, alt } = this.props; const { src, width, height, alt, lang } = this.props;
const { loading } = this.state; const { loading } = this.state;
return ( return (
@ -48,6 +49,7 @@ export default class GIFV extends React.PureComponent {
tabIndex='0' tabIndex='0'
aria-label={alt} aria-label={alt}
title={alt} title={alt}
lang={lang}
onClick={this.handleClick} onClick={this.handleClick}
/> />
)} )}
@ -58,6 +60,7 @@ export default class GIFV extends React.PureComponent {
tabIndex='0' tabIndex='0'
aria-label={alt} aria-label={alt}
title={alt} title={alt}
lang={lang}
muted muted
loop loop
autoPlay autoPlay

@ -10,6 +10,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
lang: PropTypes.string,
height: PropTypes.number, height: PropTypes.number,
width: PropTypes.number, width: PropTypes.number,
revealed: PropTypes.bool, revealed: PropTypes.bool,
@ -49,7 +50,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
}; };
render () { render () {
const { status, width, height, revealed } = this.props; const { status, lang, width, height, revealed } = this.props;
const mediaAttachments = status.get('media_attachments'); const mediaAttachments = status.get('media_attachments');
if (mediaAttachments.size === 0) { if (mediaAttachments.size === 0) {
@ -65,6 +66,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
<Component <Component
src={audio.get('url')} src={audio.get('url')}
alt={audio.get('description')} alt={audio.get('description')}
lang={lang || status.get('language')}
width={width} width={width}
height={height} height={height}
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
@ -88,6 +90,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
blurhash={video.get('blurhash')} blurhash={video.get('blurhash')}
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={video.get('description')}
lang={lang || status.get('language')}
width={width} width={width}
height={height} height={height}
inline inline
@ -104,6 +107,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
{Component => ( {Component => (
<Component <Component
media={mediaAttachments} media={mediaAttachments}
lang={lang || status.get('language')}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
defaultWidth={width} defaultWidth={width}
revealed={revealed} revealed={revealed}

@ -36,6 +36,7 @@ class Item extends React.PureComponent {
static propTypes = { static propTypes = {
attachment: ImmutablePropTypes.map.isRequired, attachment: ImmutablePropTypes.map.isRequired,
lang: PropTypes.string,
standalone: PropTypes.bool, standalone: PropTypes.bool,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
@ -98,7 +99,7 @@ class Item extends React.PureComponent {
}; };
render () { render () {
const { attachment, index, size, standalone, letterbox, displayWidth, visible } = this.props; const { attachment, lang, index, size, standalone, letterbox, displayWidth, visible } = this.props;
let width = 50; let width = 50;
let height = 100; let height = 100;
@ -154,7 +155,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
<Blurhash <Blurhash
hash={attachment.get('blurhash')} hash={attachment.get('blurhash')}
className='media-gallery__preview' className='media-gallery__preview'
@ -195,6 +196,7 @@ class Item extends React.PureComponent {
sizes={sizes} sizes={sizes}
alt={attachment.get('description')} alt={attachment.get('description')}
title={attachment.get('description')} title={attachment.get('description')}
lang={lang}
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }} style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}
/> />
@ -209,6 +211,7 @@ class Item extends React.PureComponent {
className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`} className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
aria-label={attachment.get('description')} aria-label={attachment.get('description')}
title={attachment.get('description')} title={attachment.get('description')}
lang={lang}
role='application' role='application'
src={attachment.get('url')} src={attachment.get('url')}
onClick={this.handleClick} onClick={this.handleClick}
@ -251,6 +254,7 @@ class MediaGallery extends React.PureComponent {
fullwidth: PropTypes.bool, fullwidth: PropTypes.bool,
hidden: PropTypes.bool, hidden: PropTypes.bool,
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
lang: PropTypes.string,
size: PropTypes.object, size: PropTypes.object,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -342,7 +346,7 @@ class MediaGallery extends React.PureComponent {
} }
render () { render () {
const { media, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props; const { media, lang, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props;
const { visible } = this.state; const { visible } = this.state;
const size = media.take(4).size; const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown'); const uncached = media.every(attachment => attachment.get('type') === 'unknown');
@ -364,9 +368,9 @@ class MediaGallery extends React.PureComponent {
} }
if (this.isStandaloneEligible()) { if (this.isStandaloneEligible()) {
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />;
} else { } else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />); children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} letterbox={letterbox} displayWidth={width} visible={visible || uncached} />);
} }
if (uncached) { if (uncached) {

@ -40,6 +40,7 @@ class Poll extends ImmutablePureComponent {
static propTypes = { static propTypes = {
poll: ImmutablePropTypes.map, poll: ImmutablePropTypes.map,
lang: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
disabled: PropTypes.bool, disabled: PropTypes.bool,
refresh: PropTypes.func, refresh: PropTypes.func,
@ -126,7 +127,7 @@ class Poll extends ImmutablePureComponent {
}; };
renderOption (option, optionIndex, showResults) { renderOption (option, optionIndex, showResults) {
const { poll, disabled, intl } = this.props; const { poll, lang, disabled, intl } = this.props;
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100; const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
@ -159,6 +160,7 @@ class Poll extends ImmutablePureComponent {
onKeyPress={this.handleOptionKeyPress} onKeyPress={this.handleOptionKeyPress}
aria-checked={active} aria-checked={active}
aria-label={option.get('title')} aria-label={option.get('title')}
lang={lang}
data-index={optionIndex} data-index={optionIndex}
/> />
)} )}
@ -175,6 +177,7 @@ class Poll extends ImmutablePureComponent {
<span <span
className='poll__option__text translate' className='poll__option__text translate'
lang={lang}
dangerouslySetInnerHTML={{ __html: titleEmojified }} dangerouslySetInnerHTML={{ __html: titleEmojified }}
/> />

@ -635,6 +635,7 @@ class Status extends ImmutablePureComponent {
<Component <Component
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])} foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
@ -664,6 +665,7 @@ class Status extends ImmutablePureComponent {
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
inline inline
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
@ -685,6 +687,7 @@ class Status extends ImmutablePureComponent {
{Component => ( {Component => (
<Component <Component
media={attachments} media={attachments}
lang={status.get('language')}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])} fullwidth={settings.getIn(['media', 'fullwidth'])}
@ -719,7 +722,7 @@ class Status extends ImmutablePureComponent {
} }
if (status.get('poll')) { if (status.get('poll')) {
contentMedia.push(<PollContainer pollId={status.get('poll')} />); contentMedia.push(<PollContainer pollId={status.get('poll')} lang={status.get('language')} />);
contentMediaIcons.push('tasks'); contentMediaIcons.push('tasks');
} }

@ -3,9 +3,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import Permalink from './permalink'; import Permalink from './permalink';
import { connect } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import Icon from 'flavours/glitch/components/icon'; import Icon from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'flavours/glitch/initial_state'; import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
const textMatchesTarget = (text, origin, host) => { const textMatchesTarget = (text, origin, host) => {
@ -99,7 +100,12 @@ class TranslateButton extends React.PureComponent {
} }
export default @injectIntl const mapStateToProps = state => ({
languages: state.getIn(['server', 'translationLanguages', 'items']),
});
export default @connect(mapStateToProps)
@injectIntl
class StatusContent extends React.PureComponent { class StatusContent extends React.PureComponent {
static contextTypes = { static contextTypes = {
@ -120,6 +126,7 @@ class StatusContent extends React.PureComponent {
onUpdate: PropTypes.func, onUpdate: PropTypes.func,
tagLinks: PropTypes.bool, tagLinks: PropTypes.bool,
rewriteMentions: PropTypes.string, rewriteMentions: PropTypes.string,
languages: ImmutablePropTypes.map,
intl: PropTypes.object, intl: PropTypes.object,
}; };
@ -315,7 +322,9 @@ class StatusContent extends React.PureComponent {
} = this.props; } = this.props;
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language'); const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale);
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') };

@ -56,6 +56,8 @@ const messages = defineMessages({
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' }, author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' }, matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
@ -212,7 +214,18 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}, },
onEdit (status, history) { onEdit (status, history) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.editMessage),
confirm: intl.formatMessage(messages.editConfirm),
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
}));
} else {
dispatch(editStatus(status.get('id'), history)); dispatch(editStatus(status.get('id'), history));
}
});
}, },
onTranslate (status) { onTranslate (status) {

@ -76,6 +76,7 @@ export default class MediaItem extends ImmutablePureComponent {
<img <img
src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])} src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}
/> />
); );
@ -95,6 +96,7 @@ export default class MediaItem extends ImmutablePureComponent {
<img <img
src={attachment.get('preview_url')} src={attachment.get('preview_url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
style={{ objectPosition: `${x}% ${y}%` }} style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}
/> />
@ -105,6 +107,7 @@ export default class MediaItem extends ImmutablePureComponent {
className='media-gallery__item-gifv-thumbnail' className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')} aria-label={attachment.get('description')}
title={attachment.get('description')} title={attachment.get('description')}
lang={status.get('language')}
role='application' role='application'
src={attachment.get('url')} src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}

@ -28,6 +28,7 @@ class Audio extends React.PureComponent {
static propTypes = { static propTypes = {
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
poster: PropTypes.string, poster: PropTypes.string,
duration: PropTypes.number, duration: PropTypes.number,
width: PropTypes.number, width: PropTypes.number,
@ -464,7 +465,7 @@ class Audio extends React.PureComponent {
}; };
render () { render () {
const { src, intl, alt, editable, autoPlay, sensitive, blurhash } = this.props; const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state; const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
@ -509,6 +510,7 @@ class Audio extends React.PureComponent {
onKeyDown={this.handleAudioKeyDown} onKeyDown={this.handleAudioKeyDown}
title={alt} title={alt}
aria-label={alt} aria-label={alt}
lang={lang}
/> />
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>

@ -16,10 +16,8 @@ const messages = defineMessages({
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' }, show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' }, keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' }, featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' },
}); });
@ -28,6 +26,11 @@ export default @connect()
@injectIntl @injectIntl
class gettingStartedMisc extends ImmutablePureComponent { class gettingStartedMisc extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,
identity: PropTypes.object,
};
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
@ -43,8 +46,7 @@ class gettingStartedMisc extends ImmutablePureComponent {
render () { render () {
const { intl } = this.props; const { intl } = this.props;
const { signedIn } = this.context.identity;
let i = 1;
return ( return (
<Column icon='ellipsis-h' heading={intl.formatMessage(messages.heading)}> <Column icon='ellipsis-h' heading={intl.formatMessage(messages.heading)}>
@ -52,15 +54,14 @@ class gettingStartedMisc extends ImmutablePureComponent {
<div className='scrollable'> <div className='scrollable'>
<ColumnSubheading text={intl.formatMessage(messages.subheading)} /> <ColumnSubheading text={intl.formatMessage(messages.subheading)} />
<ColumnLink key='{i++}' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' /> {signedIn && (<ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />)}
<ColumnLink key='{i++}' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' /> {signedIn && (<ColumnLink key='pinned' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />)}
<ColumnLink key='{i++}' icon='users' text={intl.formatMessage(messages.featured_users)} onClick={this.openFeaturedAccountsModal} /> {signedIn && (<ColumnLink key='featured_users' icon='users' text={intl.formatMessage(messages.featured_users)} onClick={this.openFeaturedAccountsModal} />)}
<ColumnLink key='{i++}' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' /> {signedIn && (<ColumnLink key='mutes' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />)}
<ColumnLink key='{i++}' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' /> {signedIn && (<ColumnLink key='blocks' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />)}
<ColumnLink key='{i++}' icon='minus-circle' text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' /> {signedIn && (<ColumnLink key='domain_blocks' icon='minus-circle' text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' />)}
<ColumnLink key='{i++}' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' /> <ColumnLink key='shortcuts' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />
<ColumnLink key='{i++}' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> {signedIn && (<ColumnLink key='onboarding' icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} />)}
<ColumnLink key='{i++}' icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} />
</div> </div>
</Column> </Column>
); );

@ -173,6 +173,7 @@ class DetailedStatus extends ImmutablePureComponent {
<Audio <Audio
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
@ -195,6 +196,7 @@ class DetailedStatus extends ImmutablePureComponent {
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
inline inline
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
@ -213,6 +215,7 @@ class DetailedStatus extends ImmutablePureComponent {
standalone standalone
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
media={status.get('media_attachments')} media={status.get('media_attachments')}
lang={status.get('language')}
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])} fullwidth={settings.getIn(['media', 'fullwidth'])}
hidden={!expanded} hidden={!expanded}
@ -229,7 +232,7 @@ class DetailedStatus extends ImmutablePureComponent {
} }
if (status.get('poll')) { if (status.get('poll')) {
contentMedia.push(<PollContainer pollId={status.get('poll')} />); contentMedia.push(<PollContainer pollId={status.get('poll')} lang={status.get('language')} />);
contentMediaIcons.push('tasks'); contentMediaIcons.push('tasks');
} }

@ -7,15 +7,17 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer'; import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']), accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps, null, null, { forwardRef: true })
class AudioModal extends ImmutablePureComponent { class AudioModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
statusId: PropTypes.string.isRequired, statusId: PropTypes.string.isRequired,
language: PropTypes.string,
accountStaticAvatar: PropTypes.string.isRequired, accountStaticAvatar: PropTypes.string.isRequired,
options: PropTypes.shape({ options: PropTypes.shape({
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
@ -29,7 +31,7 @@ class AudioModal extends ImmutablePureComponent {
}; };
render () { render () {
const { media, accountStaticAvatar, statusId, onClose } = this.props; const { media, language, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
return ( return (
@ -38,6 +40,7 @@ class AudioModal extends ImmutablePureComponent {
<Audio <Audio
src={media.get('url')} src={media.get('url')}
alt={media.get('description')} alt={media.get('description')}
lang={language}
duration={media.getIn(['meta', 'original', 'duration'], 0)} duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150} height={150}
poster={media.get('preview_url') || accountStaticAvatar} poster={media.get('preview_url') || accountStaticAvatar}

@ -12,6 +12,7 @@ import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import MediaAttachments from 'flavours/glitch/components/media_attachments'; import MediaAttachments from 'flavours/glitch/components/media_attachments';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
versions: state.getIn(['history', statusId, 'items']), versions: state.getIn(['history', statusId, 'items']),
}); });
@ -30,11 +31,12 @@ class CompareHistoryModal extends React.PureComponent {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
statusId: PropTypes.string.isRequired, statusId: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
versions: ImmutablePropTypes.list.isRequired, versions: ImmutablePropTypes.list.isRequired,
}; };
render () { render () {
const { index, versions, onClose } = this.props; const { index, versions, language, onClose } = this.props;
const currentVersion = versions.get(index); const currentVersion = versions.get(index);
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => { const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
@ -65,12 +67,12 @@ class CompareHistoryModal extends React.PureComponent {
<div className='status__content'> <div className='status__content'>
{currentVersion.get('spoiler_text').length > 0 && ( {currentVersion.get('spoiler_text').length > 0 && (
<React.Fragment> <React.Fragment>
<div className='translate' dangerouslySetInnerHTML={spoilerContent} /> <div className='translate' dangerouslySetInnerHTML={spoilerContent} lang={language} />
<hr /> <hr />
</React.Fragment> </React.Fragment>
)} )}
<div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} /> <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} lang={language} />
{!!currentVersion.get('poll') && ( {!!currentVersion.get('poll') && (
<div className='poll'> <div className='poll'>
@ -82,6 +84,7 @@ class CompareHistoryModal extends React.PureComponent {
<span <span
className='poll__option__text translate' className='poll__option__text translate'
dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }} dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }}
lang={language}
/> />
</li> </li>
))} ))}
@ -89,7 +92,7 @@ class CompareHistoryModal extends React.PureComponent {
</div> </div>
)} )}
<MediaAttachments status={currentVersion} /> <MediaAttachments status={currentVersion} lang={language} />
</div> </div>
</div> </div>
</div> </div>

@ -23,8 +23,8 @@ const mapDispatchToProps = (dispatch) => ({
}, },
}); });
export default @connect(null, mapDispatchToProps) export default @withRouter
@withRouter @connect(null, mapDispatchToProps)
class Header extends React.PureComponent { class Header extends React.PureComponent {
static contextTypes = { static contextTypes = {

@ -8,6 +8,7 @@ export default class ImageLoader extends PureComponent {
static propTypes = { static propTypes = {
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
previewSrc: PropTypes.string, previewSrc: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
@ -18,6 +19,7 @@ export default class ImageLoader extends PureComponent {
static defaultProps = { static defaultProps = {
alt: '', alt: '',
lang: '',
width: null, width: null,
height: null, height: null,
}; };
@ -129,7 +131,7 @@ export default class ImageLoader extends PureComponent {
}; };
render () { render () {
const { alt, src, width, height, onClick } = this.props; const { alt, lang, src, width, height, onClick } = this.props;
const { loading } = this.state; const { loading } = this.state;
const className = classNames('image-loader', { const className = classNames('image-loader', {
@ -154,6 +156,7 @@ export default class ImageLoader extends PureComponent {
) : ( ) : (
<ZoomableImage <ZoomableImage
alt={alt} alt={alt}
lang={lang}
src={src} src={src}
onClick={onClick} onClick={onClick}
width={width} width={width}

@ -3,6 +3,7 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'flavours/glitch/features/video'; import Video from 'flavours/glitch/features/video';
import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'flavours/glitch/components/icon_button'; import IconButton from 'flavours/glitch/components/icon_button';
@ -20,7 +21,12 @@ const messages = defineMessages({
next: { id: 'lightbox.next', defaultMessage: 'Next' }, next: { id: 'lightbox.next', defaultMessage: 'Next' },
}); });
export default @injectIntl const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
});
export default @connect(mapStateToProps, null, null, { forwardRef: true })
@injectIntl
class MediaModal extends ImmutablePureComponent { class MediaModal extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
@ -131,7 +137,7 @@ class MediaModal extends ImmutablePureComponent {
} }
render () { render () {
const { media, statusId, intl, onClose } = this.props; const { media, language, statusId, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden } = this.state;
const index = this.getIndex(); const index = this.getIndex();
@ -151,6 +157,7 @@ class MediaModal extends ImmutablePureComponent {
width={width} width={width}
height={height} height={height}
alt={image.get('description')} alt={image.get('description')}
lang={language}
key={image.get('url')} key={image.get('url')}
onClick={this.toggleNavigation} onClick={this.toggleNavigation}
zoomButtonHidden={this.state.zoomButtonHidden} zoomButtonHidden={this.state.zoomButtonHidden}
@ -173,6 +180,7 @@ class MediaModal extends ImmutablePureComponent {
onCloseVideo={onClose} onCloseVideo={onClose}
detailed detailed
alt={image.get('description')} alt={image.get('description')}
lang={language}
key={image.get('url')} key={image.get('url')}
/> />
); );
@ -184,6 +192,7 @@ class MediaModal extends ImmutablePureComponent {
height={height} height={height}
key={image.get('preview_url')} key={image.get('preview_url')}
alt={image.get('description')} alt={image.get('description')}
lang={language}
onClick={this.toggleNavigation} onClick={this.toggleNavigation}
/> />
); );

@ -78,8 +78,8 @@ class NavigationPanel extends React.Component {
{signedIn && ( {signedIn && (
<React.Fragment> <React.Fragment>
<ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} /> <ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} /> <ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} /> <ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
<ListPanel /> <ListPanel />

@ -2,11 +2,17 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'flavours/glitch/features/video'; import Video from 'flavours/glitch/features/video';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer'; import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'flavours/glitch/blurhash'; import { getAverageFromBlurhash } from 'flavours/glitch/blurhash';
export default class VideoModal extends ImmutablePureComponent { const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
});
export default @connect(mapStateToProps, null, null, { forwardRef: true })
class VideoModal extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
router: PropTypes.object, router: PropTypes.object,
@ -15,6 +21,7 @@ export default class VideoModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
statusId: PropTypes.string, statusId: PropTypes.string,
language: PropTypes.string,
options: PropTypes.shape({ options: PropTypes.shape({
startTime: PropTypes.number, startTime: PropTypes.number,
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
@ -35,7 +42,7 @@ export default class VideoModal extends ImmutablePureComponent {
} }
render () { render () {
const { media, statusId, onClose } = this.props; const { media, statusId, language, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
return ( return (
@ -53,6 +60,7 @@ export default class VideoModal extends ImmutablePureComponent {
autoFocus autoFocus
detailed detailed
alt={media.get('description')} alt={media.get('description')}
lang={language}
/> />
</div> </div>

@ -96,6 +96,7 @@ class ZoomableImage extends React.PureComponent {
static propTypes = { static propTypes = {
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
@ -106,6 +107,7 @@ class ZoomableImage extends React.PureComponent {
static defaultProps = { static defaultProps = {
alt: '', alt: '',
lang: '',
width: null, width: null,
height: null, height: null,
}; };
@ -403,7 +405,7 @@ class ZoomableImage extends React.PureComponent {
}; };
render () { render () {
const { alt, src, width, height, intl } = this.props; const { alt, lang, src, width, height, intl } = this.props;
const { scale, lockTranslate } = this.state; const { scale, lockTranslate } = this.state;
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll'; const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : ''; const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
@ -431,6 +433,7 @@ class ZoomableImage extends React.PureComponent {
ref={this.setImageRef} ref={this.setImageRef}
alt={alt} alt={alt}
title={alt} title={alt}
lang={lang}
src={src} src={src}
width={width} width={width}
height={height} height={height}

@ -10,7 +10,7 @@ import { debounce } from 'lodash';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose';
import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import { expandHomeTimeline } from 'flavours/glitch/actions/timelines';
import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications'; import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications';
import { fetchServer } from 'flavours/glitch/actions/server'; import { fetchServer, fetchServerTranslationLanguages } from 'flavours/glitch/actions/server';
import { clearHeight } from 'flavours/glitch/actions/height_cache'; import { clearHeight } from 'flavours/glitch/actions/height_cache';
import { changeLayout } from 'flavours/glitch/actions/app'; import { changeLayout } from 'flavours/glitch/actions/app';
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
@ -419,6 +419,7 @@ class UI extends React.Component {
this.props.dispatch(fetchMarkers()); this.props.dispatch(fetchMarkers());
this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications()); this.props.dispatch(expandNotifications());
this.props.dispatch(fetchServerTranslationLanguages());
setTimeout(() => this.props.dispatch(fetchServer()), 3000); setTimeout(() => this.props.dispatch(fetchServer()), 3000);
} }

@ -101,6 +101,7 @@ class Video extends React.PureComponent {
frameRate: PropTypes.string, frameRate: PropTypes.string,
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
sensitive: PropTypes.bool, sensitive: PropTypes.bool,
@ -538,7 +539,7 @@ class Video extends React.PureComponent {
} }
render () { render () {
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props; const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
const playerStyle = {}; const playerStyle = {};
@ -603,6 +604,7 @@ class Video extends React.PureComponent {
tabIndex='0' tabIndex='0'
aria-label={alt} aria-label={alt}
title={alt} title={alt}
lang={lang}
width={width} width={width}
height={height} height={height}
volume={volume} volume={volume}

@ -67,7 +67,6 @@
"moved_to_warning": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.", "moved_to_warning": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.",
"navigation_bar.app_settings": "App settings", "navigation_bar.app_settings": "App settings",
"navigation_bar.featured_users": "Featured users", "navigation_bar.featured_users": "Featured users",
"navigation_bar.info": "Extended information",
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts", "navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
"navigation_bar.misc": "Misc", "navigation_bar.misc": "Misc",
"notification.markForDeletion": "Mark for deletion", "notification.markForDeletion": "Mark for deletion",

@ -8,12 +8,13 @@
"advanced_options.icon_title": "Opciones avanzadas", "advanced_options.icon_title": "Opciones avanzadas",
"advanced_options.local-only.long": "No publicar a otras instancias", "advanced_options.local-only.long": "No publicar a otras instancias",
"advanced_options.local-only.short": "Local", "advanced_options.local-only.short": "Local",
"advanced_options.local-only.tooltip": "Este toot es local", "advanced_options.local-only.tooltip": "Esta publicación es local",
"advanced_options.threaded_mode.long": "Al publicar abre automáticamente una respuesta", "advanced_options.threaded_mode.long": "Al publicar abre automáticamente una respuesta",
"advanced_options.threaded_mode.short": "Modo hilo", "advanced_options.threaded_mode.short": "Modo hilo",
"advanced_options.threaded_mode.tooltip": "Modo hilo habilitado", "advanced_options.threaded_mode.tooltip": "Modo hilo habilitado",
"boost_modal.missing_description": "Esta publicación contiene medios sin descripción",
"column.favourited_by": "Marcado como favorito por", "column.favourited_by": "Marcado como favorito por",
"column.reblogged_by": "Retooteado por", "column.reblogged_by": "Impulsado por",
"column_header.profile": "Perfil", "column_header.profile": "Perfil",
"column_subheading.lists": "Listas", "column_subheading.lists": "Listas",
"column_subheading.navigation": "Navegación", "column_subheading.navigation": "Navegación",
@ -50,26 +51,26 @@
"settings.always_show_spoilers_field": "Siempre mostrar el campo de advertencia de contenido", "settings.always_show_spoilers_field": "Siempre mostrar el campo de advertencia de contenido",
"settings.auto_collapse": "Colapsar automáticamente", "settings.auto_collapse": "Colapsar automáticamente",
"settings.auto_collapse_all": "Todo", "settings.auto_collapse_all": "Todo",
"settings.auto_collapse_lengthy": "Toots largos", "settings.auto_collapse_lengthy": "Publicaciones largas",
"settings.auto_collapse_media": "Toots con medios", "settings.auto_collapse_media": "Publicaciones multimedia",
"settings.auto_collapse_notifications": "Notificaciones", "settings.auto_collapse_notifications": "Notificaciones",
"settings.auto_collapse_reblogs": "Retoots", "settings.auto_collapse_reblogs": "Impulsos",
"settings.auto_collapse_replies": "Respuestas", "settings.auto_collapse_replies": "Respuestas",
"settings.close": "Cerrar", "settings.close": "Cerrar",
"settings.collapsed_statuses": "Toots colapsados", "settings.collapsed_statuses": "Publicaciones colapsadas",
"settings.compose_box_opts": "Cuadro de redacción", "settings.compose_box_opts": "Cuadro de redacción",
"settings.confirm_before_clearing_draft": "Mostrar diálogo de confirmación antes de sobreescribir un mensaje estabas escribiendo", "settings.confirm_before_clearing_draft": "Mostrar diálogo de confirmación antes de sobreescribir el mensaje siendo redactado",
"settings.confirm_boost_missing_media_description": "Mostrar diálogo de confirmación antes de retootear publicaciones con medios sin descripción", "settings.confirm_boost_missing_media_description": "Mostrar diálogo de confirmación antes de impulsar publicaciones con medios sin descripciones",
"settings.confirm_missing_media_description": "Mostrar diálogo de confirmación antes de publicar toots con medios sin descripción", "settings.confirm_missing_media_description": "Mostrar diálogo de confirmación antes de enviar publicaciones con medios sin descripciones",
"settings.content_warnings": "Advertencias de contenido", "settings.content_warnings": "Advertencias de contenido",
"settings.content_warnings.regexp": "Regexp (expresión regular)", "settings.content_warnings.regexp": "Regexp (expresión regular)",
"settings.content_warnings_filter": "No descolapsar estas advertencias de contenido:", "settings.content_warnings_filter": "No descolapsar estas advertencias de contenido:",
"settings.enable_collapsed": "Habilitar toots colapsados", "settings.enable_collapsed": "Habilitar publicaciones colapsadas",
"settings.enable_content_warnings_auto_unfold": "Descolapsar automáticamente advertencias de contenido", "settings.enable_content_warnings_auto_unfold": "Desplegar automáticamente advertencias de contenido",
"settings.hicolor_privacy_icons": "Íconos de privacidad más visibles", "settings.hicolor_privacy_icons": "Íconos de privacidad más visibles",
"settings.image_backgrounds": "Fondos de imágenes", "settings.image_backgrounds": "Fondos de imágenes",
"settings.image_backgrounds_media": "Vista previa de medios de toots colapsados", "settings.image_backgrounds_media": "Vista previa de medios de publicaciones colapsadas",
"settings.image_backgrounds_users": "Darle fondo de imagen a toots colapsados", "settings.image_backgrounds_users": "Darle fondo de imagen a publicaciones colapsadas",
"settings.inline_preview_cards": "Vista previa para enlaces externos", "settings.inline_preview_cards": "Vista previa para enlaces externos",
"settings.layout": "Diseño", "settings.layout": "Diseño",
"settings.layout_opts": "Opciones de diseño", "settings.layout_opts": "Opciones de diseño",
@ -91,21 +92,21 @@
"settings.rewrite_mentions_acct": "Reescribir con nombre de usuarix y dominio (para cuentas remotas)", "settings.rewrite_mentions_acct": "Reescribir con nombre de usuarix y dominio (para cuentas remotas)",
"settings.rewrite_mentions_no": "No reescribir menciones", "settings.rewrite_mentions_no": "No reescribir menciones",
"settings.rewrite_mentions_username": "Reescribir con nombre de usuarix", "settings.rewrite_mentions_username": "Reescribir con nombre de usuarix",
"settings.show_action_bar": "Mostrar botones de acción en toots colapsados", "settings.show_action_bar": "Mostrar botones de acción en publicaciones colapsadas",
"settings.show_content_type_choice": "Mostrar selección de tipo de contenido al crear toots", "settings.show_content_type_choice": "Mostrar selección de tipo de contenido al crear publicaciones",
"settings.show_reply_counter": "Mostrar un conteo estimado de respuestas", "settings.show_reply_counter": "Mostrar un conteo estimado de respuestas",
"settings.side_arm": "Botón secundario:", "settings.side_arm": "Botón secundario:",
"settings.side_arm.none": "Ninguno", "settings.side_arm.none": "Ninguno",
"settings.side_arm_reply_mode": "Al responder a un toot, el botón de toot secundario debe:", "settings.side_arm_reply_mode": "Al responder a una publicación, el botón de publicación secundario debe:",
"settings.side_arm_reply_mode.copy": "Copiar opción de privacidad del toot al que estás respondiendo", "settings.side_arm_reply_mode.copy": "Copiar opción de privacidad de la publicación a la que estás respondiendo",
"settings.side_arm_reply_mode.keep": "Conservar opción de privacidad", "settings.side_arm_reply_mode.keep": "Conservar opción de privacidad",
"settings.side_arm_reply_mode.restrict": "Restringir la opción de privacidad a la misma del toot al que estás respondiendo", "settings.side_arm_reply_mode.restrict": "Restringir la opción de privacidad a la misma de la publicación a la que estás respondiendo",
"settings.swipe_to_change_columns": "Permitir deslizar para cambiar columnas (Sólo en móvil)", "settings.swipe_to_change_columns": "Permitir deslizar para cambiar columnas (Sólo en móvil)",
"settings.tag_misleading_links": "Marcar enlaces engañosos", "settings.tag_misleading_links": "Marcar enlaces engañosos",
"settings.tag_misleading_links.hint": "Añadir una indicación visual indicando el destino de los enlace que no los mencionen explícitamente", "settings.tag_misleading_links.hint": "Añadir una indicación visual indicando el destino de los enlace que no los mencionen explícitamente",
"settings.wide_view": "Vista amplia (solo modo de escritorio)", "settings.wide_view": "Vista amplia (solo modo de escritorio)",
"status.collapse": "Colapsar", "status.collapse": "Colapsar",
"status.in_reply_to": "Este toot es una respuesta", "status.in_reply_to": "Esta publicación es una respuesta",
"status.is_poll": "Este toot es una encuesta", "status.is_poll": "Esta publicación es una encuesta",
"status.uncollapse": "Descolapsar" "status.uncollapse": "Descolapsar"
} }

@ -2,6 +2,9 @@ import {
SERVER_FETCH_REQUEST, SERVER_FETCH_REQUEST,
SERVER_FETCH_SUCCESS, SERVER_FETCH_SUCCESS,
SERVER_FETCH_FAIL, SERVER_FETCH_FAIL,
SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
EXTENDED_DESCRIPTION_REQUEST, EXTENDED_DESCRIPTION_REQUEST,
EXTENDED_DESCRIPTION_SUCCESS, EXTENDED_DESCRIPTION_SUCCESS,
EXTENDED_DESCRIPTION_FAIL, EXTENDED_DESCRIPTION_FAIL,
@ -35,6 +38,12 @@ export default function server(state = initialState, action) {
return state.set('server', fromJS(action.server)).setIn(['server', 'isLoading'], false); return state.set('server', fromJS(action.server)).setIn(['server', 'isLoading'], false);
case SERVER_FETCH_FAIL: case SERVER_FETCH_FAIL:
return state.setIn(['server', 'isLoading'], false); return state.setIn(['server', 'isLoading'], false);
case SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST:
return state.setIn(['translationLanguages', 'isLoading'], true);
case SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS:
return state.setIn(['translationLanguages', 'items'], fromJS(action.translationLanguages)).setIn(['translationLanguages', 'isLoading'], false);
case SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL:
return state.setIn(['translationLanguages', 'isLoading'], false);
case EXTENDED_DESCRIPTION_REQUEST: case EXTENDED_DESCRIPTION_REQUEST:
return state.setIn(['extendedDescription', 'isLoading'], true); return state.setIn(['extendedDescription', 'isLoading'], true);
case EXTENDED_DESCRIPTION_SUCCESS: case EXTENDED_DESCRIPTION_SUCCESS:

@ -168,8 +168,7 @@
white-space: pre-wrap; white-space: pre-wrap;
p, p,
pre, pre {
blockquote {
margin-bottom: 20px; margin-bottom: 20px;
white-space: pre-wrap; white-space: pre-wrap;
@ -178,79 +177,6 @@
} }
} }
h1,
h2,
h3,
h4,
h5 {
margin-top: 20px;
margin-bottom: 20px;
}
h1,
h2 {
font-weight: 700;
font-size: 18px;
}
h2 {
font-size: 16px;
}
h3,
h4,
h5 {
font-weight: 500;
}
blockquote {
padding-left: 10px;
border-left: 3px solid $inverted-text-color;
color: $inverted-text-color;
white-space: normal;
p:last-child {
margin-bottom: 0;
}
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul,
ol {
margin-left: 1em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
a { a {
color: $lighter-text-color; color: $lighter-text-color;
text-decoration: none; text-decoration: none;

@ -1628,6 +1628,7 @@ button.icon-button.active i.fa-retweet {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center;
color: $secondary-text-color; color: $secondary-text-color;
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;

@ -26,11 +26,13 @@
img { img {
margin-top: 2em; margin-top: 2em;
margin-bottom: 2em; margin-bottom: 2em;
max-width: 100%;
} }
video { video {
margin-top: 2em; margin-top: 2em;
margin-bottom: 2em; margin-bottom: 2em;
max-width: 100%;
} }
figure { figure {

@ -68,8 +68,7 @@
} }
p, p,
pre, pre {
blockquote {
margin-bottom: 20px; margin-bottom: 20px;
white-space: pre-wrap; white-space: pre-wrap;
unicode-bidi: plaintext; unicode-bidi: plaintext;

@ -21,3 +21,4 @@
@import 'accessibility'; @import 'accessibility';
@import 'rtl'; @import 'rtl';
@import 'dashboard'; @import 'dashboard';
@import 'rich_text';

@ -0,0 +1,99 @@
.status__content__text,
.e-content,
.reply-indicator__content {
pre,
blockquote {
margin-bottom: 20px;
white-space: pre-wrap;
unicode-bidi: plaintext;
&:last-child {
margin-bottom: 0;
}
}
blockquote {
padding-left: 10px;
border-left: 3px solid $darker-text-color;
color: $darker-text-color;
white-space: normal;
p:last-child {
margin-bottom: 0;
}
}
& > ul,
& > ol {
margin-bottom: 20px;
}
h1,
h2,
h3,
h4,
h5 {
margin-top: 20px;
margin-bottom: 20px;
}
h1,
h2 {
font-weight: 700;
font-size: 1.2em;
}
h2 {
font-size: 1.1em;
}
h3,
h4,
h5 {
font-weight: 500;
}
b,
strong {
font-weight: 700;
}
em,
i {
font-style: italic;
}
sub {
font-size: smaller;
vertical-align: sub;
}
sup {
font-size: smaller;
vertical-align: super;
}
ul,
ol {
margin-left: 2em;
p {
margin: 0;
}
}
ul {
list-style-type: disc;
}
ol {
list-style-type: decimal;
}
}
.reply-indicator__content {
blockquote {
border-left-color: $inverted-text-color;
color: $inverted-text-color;
}
}

@ -5,6 +5,10 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL';
export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
@ -37,6 +41,29 @@ const fetchServerFail = error => ({
error, error,
}); });
export const fetchServerTranslationLanguages = () => (dispatch, getState) => {
dispatch(fetchServerTranslationLanguagesRequest());
api(getState)
.get('/api/v1/instance/translation_languages').then(({ data }) => {
dispatch(fetchServerTranslationLanguagesSuccess(data));
}).catch(err => dispatch(fetchServerTranslationLanguagesFail(err)));
};
const fetchServerTranslationLanguagesRequest = () => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
});
const fetchServerTranslationLanguagesSuccess = translationLanguages => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
translationLanguages,
});
const fetchServerTranslationLanguagesFail = error => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
error,
});
export const fetchExtendedDescription = () => (dispatch, getState) => { export const fetchExtendedDescription = () => (dispatch, getState) => {
dispatch(fetchExtendedDescriptionRequest()); dispatch(fetchExtendedDescriptionRequest());

@ -15,10 +15,10 @@ export default class ColumnBackButton extends React.PureComponent {
}; };
handleClick = () => { handleClick = () => {
if (window.history && window.history.length === 1) { if (window.history && window.history.state) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack(); this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
} }
}; };

@ -43,14 +43,6 @@ class ColumnHeader extends React.PureComponent {
animating: false, animating: false,
}; };
historyBack = () => {
if (window.history && window.history.length === 1) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack();
}
};
handleToggleClick = (e) => { handleToggleClick = (e) => {
e.stopPropagation(); e.stopPropagation();
this.setState({ collapsed: !this.state.collapsed, animating: true }); this.setState({ collapsed: !this.state.collapsed, animating: true });
@ -69,7 +61,11 @@ class ColumnHeader extends React.PureComponent {
}; };
handleBackClick = () => { handleBackClick = () => {
this.historyBack(); if (window.history && window.history.state) {
this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
}
}; };
handleTransitionEnd = () => { handleTransitionEnd = () => {

@ -6,6 +6,7 @@ export default class GIFV extends React.PureComponent {
static propTypes = { static propTypes = {
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
onClick: PropTypes.func, onClick: PropTypes.func,
@ -35,7 +36,7 @@ export default class GIFV extends React.PureComponent {
}; };
render () { render () {
const { src, width, height, alt } = this.props; const { src, width, height, alt, lang } = this.props;
const { loading } = this.state; const { loading } = this.state;
return ( return (
@ -48,6 +49,7 @@ export default class GIFV extends React.PureComponent {
tabIndex='0' tabIndex='0'
aria-label={alt} aria-label={alt}
title={alt} title={alt}
lang={lang}
onClick={this.handleClick} onClick={this.handleClick}
/> />
)} )}
@ -58,6 +60,7 @@ export default class GIFV extends React.PureComponent {
tabIndex='0' tabIndex='0'
aria-label={alt} aria-label={alt}
title={alt} title={alt}
lang={lang}
muted muted
loop loop
autoPlay autoPlay

@ -10,6 +10,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
lang: PropTypes.string,
height: PropTypes.number, height: PropTypes.number,
width: PropTypes.number, width: PropTypes.number,
}; };
@ -48,7 +49,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
}; };
render () { render () {
const { status, width, height } = this.props; const { status, lang, width, height } = this.props;
const mediaAttachments = status.get('media_attachments'); const mediaAttachments = status.get('media_attachments');
if (mediaAttachments.size === 0) { if (mediaAttachments.size === 0) {
@ -64,6 +65,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
<Component <Component
src={audio.get('url')} src={audio.get('url')}
alt={audio.get('description')} alt={audio.get('description')}
lang={lang || status.get('language')}
width={width} width={width}
height={height} height={height}
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
@ -87,6 +89,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
blurhash={video.get('blurhash')} blurhash={video.get('blurhash')}
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={video.get('description')}
lang={lang || status.get('language')}
width={width} width={width}
height={height} height={height}
inline inline
@ -102,6 +105,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
{Component => ( {Component => (
<Component <Component
media={mediaAttachments} media={mediaAttachments}
lang={lang || status.get('language')}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
defaultWidth={width} defaultWidth={width}
height={height} height={height}

@ -17,6 +17,7 @@ class Item extends React.PureComponent {
static propTypes = { static propTypes = {
attachment: ImmutablePropTypes.map.isRequired, attachment: ImmutablePropTypes.map.isRequired,
lang: PropTypes.string,
standalone: PropTypes.bool, standalone: PropTypes.bool,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
@ -78,7 +79,7 @@ class Item extends React.PureComponent {
}; };
render () { render () {
const { attachment, index, size, standalone, displayWidth, visible } = this.props; const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
let width = 50; let width = 50;
let height = 100; let height = 100;
@ -134,7 +135,7 @@ class Item extends React.PureComponent {
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
<Blurhash <Blurhash
hash={attachment.get('blurhash')} hash={attachment.get('blurhash')}
className='media-gallery__preview' className='media-gallery__preview'
@ -174,6 +175,7 @@ class Item extends React.PureComponent {
sizes={sizes} sizes={sizes}
alt={attachment.get('description')} alt={attachment.get('description')}
title={attachment.get('description')} title={attachment.get('description')}
lang={lang}
style={{ objectPosition: `${x}% ${y}%` }} style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}
/> />
@ -188,6 +190,7 @@ class Item extends React.PureComponent {
className='media-gallery__item-gifv-thumbnail' className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')} aria-label={attachment.get('description')}
title={attachment.get('description')} title={attachment.get('description')}
lang={lang}
role='application' role='application'
src={attachment.get('url')} src={attachment.get('url')}
onClick={this.handleClick} onClick={this.handleClick}
@ -227,6 +230,7 @@ class MediaGallery extends React.PureComponent {
sensitive: PropTypes.bool, sensitive: PropTypes.bool,
standalone: PropTypes.bool, standalone: PropTypes.bool,
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
lang: PropTypes.string,
size: PropTypes.object, size: PropTypes.object,
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
@ -310,9 +314,8 @@ class MediaGallery extends React.PureComponent {
} }
render () { render () {
const { media, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props; const { media, lang, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
const { visible } = this.state; const { visible } = this.state;
const width = this.state.width || defaultWidth; const width = this.state.width || defaultWidth;
let children, spoilerButton; let children, spoilerButton;
@ -333,9 +336,9 @@ class MediaGallery extends React.PureComponent {
const uncached = media.every(attachment => attachment.get('type') === 'unknown'); const uncached = media.every(attachment => attachment.get('type') === 'unknown');
if (standalone && this.isFullSizeEligible()) { if (standalone && this.isFullSizeEligible()) {
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} displayWidth={width} visible={visible} />; children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />;
} else { } else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} size={size} displayWidth={width} visible={visible || uncached} />); children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />);
} }
if (uncached) { if (uncached) {

@ -40,6 +40,7 @@ class Poll extends ImmutablePureComponent {
static propTypes = { static propTypes = {
poll: ImmutablePropTypes.map, poll: ImmutablePropTypes.map,
lang: PropTypes.string,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
disabled: PropTypes.bool, disabled: PropTypes.bool,
refresh: PropTypes.func, refresh: PropTypes.func,
@ -126,7 +127,7 @@ class Poll extends ImmutablePureComponent {
}; };
renderOption (option, optionIndex, showResults) { renderOption (option, optionIndex, showResults) {
const { poll, disabled, intl } = this.props; const { poll, lang, disabled, intl } = this.props;
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100; const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
@ -159,6 +160,7 @@ class Poll extends ImmutablePureComponent {
onKeyPress={this.handleOptionKeyPress} onKeyPress={this.handleOptionKeyPress}
aria-checked={active} aria-checked={active}
aria-label={option.get('title')} aria-label={option.get('title')}
lang={lang}
data-index={optionIndex} data-index={optionIndex}
/> />
)} )}
@ -175,6 +177,7 @@ class Poll extends ImmutablePureComponent {
<span <span
className='poll__option__text translate' className='poll__option__text translate'
lang={lang}
dangerouslySetInnerHTML={{ __html: titleEmojified }} dangerouslySetInnerHTML={{ __html: titleEmojified }}
/> />

@ -422,6 +422,7 @@ class Status extends ImmutablePureComponent {
<Component <Component
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])} foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
@ -451,6 +452,7 @@ class Status extends ImmutablePureComponent {
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
width={this.props.cachedMediaWidth} width={this.props.cachedMediaWidth}
height={110} height={110}
inline inline
@ -470,6 +472,7 @@ class Status extends ImmutablePureComponent {
{Component => ( {Component => (
<Component <Component
media={status.get('media_attachments')} media={status.get('media_attachments')}
lang={status.get('language')}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
height={110} height={110}
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}

@ -3,10 +3,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container'; import PollContainer from 'mastodon/containers/poll_container';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { autoPlayGif, languages as preloadedLanguages, translationEnabled } from 'mastodon/initial_state'; import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
@ -47,7 +48,12 @@ class TranslateButton extends React.PureComponent {
} }
export default @injectIntl const mapStateToProps = state => ({
languages: state.getIn(['server', 'translationLanguages', 'items']),
});
export default @connect(mapStateToProps)
@injectIntl
class StatusContent extends React.PureComponent { class StatusContent extends React.PureComponent {
static contextTypes = { static contextTypes = {
@ -63,6 +69,7 @@ class StatusContent extends React.PureComponent {
onClick: PropTypes.func, onClick: PropTypes.func,
collapsable: PropTypes.bool, collapsable: PropTypes.bool,
onCollapsedToggle: PropTypes.func, onCollapsedToggle: PropTypes.func,
languages: ImmutablePropTypes.map,
intl: PropTypes.object, intl: PropTypes.object,
}; };
@ -220,7 +227,9 @@ class StatusContent extends React.PureComponent {
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed'); const renderReadMore = this.props.onClick && status.get('collapsed');
const renderTranslate = translationEnabled && this.context.identity.signedIn && this.props.onTranslate && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && status.get('language') !== null && intl.locale !== status.get('language'); const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale);
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') };
@ -242,7 +251,7 @@ class StatusContent extends React.PureComponent {
); );
const poll = !!status.get('poll') && ( const poll = !!status.get('poll') && (
<PollContainer pollId={status.get('poll')} /> <PollContainer pollId={status.get('poll')} lang={status.get('language')} />
); );
if (status.get('spoiler_text').length > 0) { if (status.get('spoiler_text').length > 0) {

@ -58,6 +58,8 @@ const messages = defineMessages({
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
}); });
@ -160,7 +162,18 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
}, },
onEdit (status, history) { onEdit (status, history) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.editMessage),
confirm: intl.formatMessage(messages.editConfirm),
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
}));
} else {
dispatch(editStatus(status.get('id'), history)); dispatch(editStatus(status.get('id'), history));
}
});
}, },
onTranslate (status) { onTranslate (status) {

@ -76,6 +76,7 @@ export default class MediaItem extends ImmutablePureComponent {
<img <img
src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])} src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}
/> />
); );
@ -95,6 +96,7 @@ export default class MediaItem extends ImmutablePureComponent {
<img <img
src={attachment.get('preview_url')} src={attachment.get('preview_url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
style={{ objectPosition: `${x}% ${y}%` }} style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}
/> />
@ -105,6 +107,7 @@ export default class MediaItem extends ImmutablePureComponent {
className='media-gallery__item-gifv-thumbnail' className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')} aria-label={attachment.get('description')}
title={attachment.get('description')} title={attachment.get('description')}
lang={status.get('language')}
role='application' role='application'
src={attachment.get('url')} src={attachment.get('url')}
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}

@ -28,6 +28,7 @@ class Audio extends React.PureComponent {
static propTypes = { static propTypes = {
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
poster: PropTypes.string, poster: PropTypes.string,
duration: PropTypes.number, duration: PropTypes.number,
width: PropTypes.number, width: PropTypes.number,
@ -458,7 +459,7 @@ class Audio extends React.PureComponent {
}; };
render () { render () {
const { src, intl, alt, editable, autoPlay, sensitive, blurhash } = this.props; const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state; const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
@ -503,6 +504,7 @@ class Audio extends React.PureComponent {
onKeyDown={this.handleAudioKeyDown} onKeyDown={this.handleAudioKeyDown}
title={alt} title={alt}
aria-label={alt} aria-label={alt}
lang={lang}
/> />
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>

@ -143,6 +143,7 @@ class DetailedStatus extends ImmutablePureComponent {
<Audio <Audio
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
@ -165,6 +166,7 @@ class DetailedStatus extends ImmutablePureComponent {
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={attachment.get('description')}
lang={status.get('language')}
width={300} width={300}
height={150} height={150}
inline inline
@ -180,6 +182,7 @@ class DetailedStatus extends ImmutablePureComponent {
standalone standalone
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
media={status.get('media_attachments')} media={status.get('media_attachments')}
lang={status.get('language')}
height={300} height={300}
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
visible={this.props.showMedia} visible={this.props.showMedia}

@ -7,15 +7,17 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import Footer from 'mastodon/features/picture_in_picture/components/footer'; import Footer from 'mastodon/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']), accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
}); });
export default @connect(mapStateToProps) export default @connect(mapStateToProps, null, null, { forwardRef: true })
class AudioModal extends ImmutablePureComponent { class AudioModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
statusId: PropTypes.string.isRequired, statusId: PropTypes.string.isRequired,
language: PropTypes.string,
accountStaticAvatar: PropTypes.string.isRequired, accountStaticAvatar: PropTypes.string.isRequired,
options: PropTypes.shape({ options: PropTypes.shape({
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
@ -25,7 +27,7 @@ class AudioModal extends ImmutablePureComponent {
}; };
render () { render () {
const { media, accountStaticAvatar, statusId, onClose } = this.props; const { media, language, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
return ( return (
@ -34,6 +36,7 @@ class AudioModal extends ImmutablePureComponent {
<Audio <Audio
src={media.get('url')} src={media.get('url')}
alt={media.get('description')} alt={media.get('description')}
lang={language}
duration={media.getIn(['meta', 'original', 'duration'], 0)} duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150} height={150}
poster={media.get('preview_url') || accountStaticAvatar} poster={media.get('preview_url') || accountStaticAvatar}

@ -12,6 +12,7 @@ import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import MediaAttachments from 'mastodon/components/media_attachments'; import MediaAttachments from 'mastodon/components/media_attachments';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
versions: state.getIn(['history', statusId, 'items']), versions: state.getIn(['history', statusId, 'items']),
}); });
@ -30,11 +31,12 @@ class CompareHistoryModal extends React.PureComponent {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
statusId: PropTypes.string.isRequired, statusId: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
versions: ImmutablePropTypes.list.isRequired, versions: ImmutablePropTypes.list.isRequired,
}; };
render () { render () {
const { index, versions, onClose } = this.props; const { index, versions, language, onClose } = this.props;
const currentVersion = versions.get(index); const currentVersion = versions.get(index);
const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => { const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => {
@ -65,12 +67,12 @@ class CompareHistoryModal extends React.PureComponent {
<div className='status__content'> <div className='status__content'>
{currentVersion.get('spoiler_text').length > 0 && ( {currentVersion.get('spoiler_text').length > 0 && (
<React.Fragment> <React.Fragment>
<div className='translate' dangerouslySetInnerHTML={spoilerContent} /> <div className='translate' dangerouslySetInnerHTML={spoilerContent} lang={language} />
<hr /> <hr />
</React.Fragment> </React.Fragment>
)} )}
<div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} /> <div className='status__content__text status__content__text--visible translate' dangerouslySetInnerHTML={content} lang={language} />
{!!currentVersion.get('poll') && ( {!!currentVersion.get('poll') && (
<div className='poll'> <div className='poll'>
@ -82,6 +84,7 @@ class CompareHistoryModal extends React.PureComponent {
<span <span
className='poll__option__text translate' className='poll__option__text translate'
dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }} dangerouslySetInnerHTML={{ __html: emojify(escapeTextContentForBrowser(option.get('title')), emojiMap) }}
lang={language}
/> />
</li> </li>
))} ))}
@ -89,7 +92,7 @@ class CompareHistoryModal extends React.PureComponent {
</div> </div>
)} )}
<MediaAttachments status={currentVersion} /> <MediaAttachments status={currentVersion} lang={language} />
</div> </div>
</div> </div>
</div> </div>

@ -22,8 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
}, },
}); });
export default @connect(null, mapDispatchToProps) export default @withRouter
@withRouter @connect(null, mapDispatchToProps)
class Header extends React.PureComponent { class Header extends React.PureComponent {
static contextTypes = { static contextTypes = {

@ -8,6 +8,7 @@ export default class ImageLoader extends PureComponent {
static propTypes = { static propTypes = {
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
previewSrc: PropTypes.string, previewSrc: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
@ -18,6 +19,7 @@ export default class ImageLoader extends PureComponent {
static defaultProps = { static defaultProps = {
alt: '', alt: '',
lang: '',
width: null, width: null,
height: null, height: null,
}; };
@ -129,7 +131,7 @@ export default class ImageLoader extends PureComponent {
}; };
render () { render () {
const { alt, src, width, height, onClick } = this.props; const { alt, lang, src, width, height, onClick } = this.props;
const { loading } = this.state; const { loading } = this.state;
const className = classNames('image-loader', { const className = classNames('image-loader', {
@ -154,6 +156,7 @@ export default class ImageLoader extends PureComponent {
) : ( ) : (
<ZoomableImage <ZoomableImage
alt={alt} alt={alt}
lang={lang}
src={src} src={src}
onClick={onClick} onClick={onClick}
width={width} width={width}

@ -3,6 +3,7 @@ import ReactSwipeableViews from 'react-swipeable-views';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'mastodon/features/video'; import Video from 'mastodon/features/video';
import { connect } from 'react-redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button'; import IconButton from 'mastodon/components/icon_button';
@ -20,7 +21,12 @@ const messages = defineMessages({
next: { id: 'lightbox.next', defaultMessage: 'Next' }, next: { id: 'lightbox.next', defaultMessage: 'Next' },
}); });
export default @injectIntl const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
});
export default @connect(mapStateToProps, null, null, { forwardRef: true })
@injectIntl
class MediaModal extends ImmutablePureComponent { class MediaModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
@ -129,7 +135,7 @@ class MediaModal extends ImmutablePureComponent {
}; };
render () { render () {
const { media, statusId, intl, onClose } = this.props; const { media, language, statusId, intl, onClose } = this.props;
const { navigationHidden } = this.state; const { navigationHidden } = this.state;
const index = this.getIndex(); const index = this.getIndex();
@ -149,6 +155,7 @@ class MediaModal extends ImmutablePureComponent {
width={width} width={width}
height={height} height={height}
alt={image.get('description')} alt={image.get('description')}
lang={language}
key={image.get('url')} key={image.get('url')}
onClick={this.toggleNavigation} onClick={this.toggleNavigation}
zoomButtonHidden={this.state.zoomButtonHidden} zoomButtonHidden={this.state.zoomButtonHidden}
@ -171,6 +178,7 @@ class MediaModal extends ImmutablePureComponent {
onCloseVideo={onClose} onCloseVideo={onClose}
detailed detailed
alt={image.get('description')} alt={image.get('description')}
lang={language}
key={image.get('url')} key={image.get('url')}
/> />
); );
@ -182,6 +190,7 @@ class MediaModal extends ImmutablePureComponent {
height={height} height={height}
key={image.get('preview_url')} key={image.get('preview_url')}
alt={image.get('description')} alt={image.get('description')}
lang={language}
onClick={this.toggleNavigation} onClick={this.toggleNavigation}
/> />
); );

@ -82,8 +82,8 @@ class NavigationPanel extends React.Component {
{signedIn && ( {signedIn && (
<React.Fragment> <React.Fragment>
<ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} /> <ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} /> <ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} /> <ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
<ListPanel /> <ListPanel />

@ -2,15 +2,22 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Video from 'mastodon/features/video'; import Video from 'mastodon/features/video';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Footer from 'mastodon/features/picture_in_picture/components/footer'; import Footer from 'mastodon/features/picture_in_picture/components/footer';
import { getAverageFromBlurhash } from 'mastodon/blurhash'; import { getAverageFromBlurhash } from 'mastodon/blurhash';
export default class VideoModal extends ImmutablePureComponent { const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']),
});
export default @connect(mapStateToProps, null, null, { forwardRef: true })
class VideoModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
statusId: PropTypes.string, statusId: PropTypes.string,
language: PropTypes.string,
options: PropTypes.shape({ options: PropTypes.shape({
startTime: PropTypes.number, startTime: PropTypes.number,
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
@ -31,7 +38,7 @@ export default class VideoModal extends ImmutablePureComponent {
} }
render () { render () {
const { media, statusId, onClose } = this.props; const { media, statusId, language, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
return ( return (
@ -49,6 +56,7 @@ export default class VideoModal extends ImmutablePureComponent {
autoFocus autoFocus
detailed detailed
alt={media.get('description')} alt={media.get('description')}
lang={language}
/> />
</div> </div>

@ -96,6 +96,7 @@ class ZoomableImage extends React.PureComponent {
static propTypes = { static propTypes = {
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
@ -106,6 +107,7 @@ class ZoomableImage extends React.PureComponent {
static defaultProps = { static defaultProps = {
alt: '', alt: '',
lang: '',
width: null, width: null,
height: null, height: null,
}; };
@ -403,7 +405,7 @@ class ZoomableImage extends React.PureComponent {
}; };
render () { render () {
const { alt, src, width, height, intl } = this.props; const { alt, lang, src, width, height, intl } = this.props;
const { scale, lockTranslate } = this.state; const { scale, lockTranslate } = this.state;
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll'; const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : ''; const zoomButtonShouldHide = this.state.navigationHidden || this.props.zoomButtonHidden || this.state.zoomMatrix.rate <= MIN_SCALE ? 'media-modal__zoom-button--hidden' : '';
@ -431,6 +433,7 @@ class ZoomableImage extends React.PureComponent {
ref={this.setImageRef} ref={this.setImageRef}
alt={alt} alt={alt}
title={alt} title={alt}
lang={lang}
src={src} src={src}
width={width} width={width}
height={height} height={height}

@ -13,7 +13,7 @@ import { debounce } from 'lodash';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { expandHomeTimeline } from '../../actions/timelines'; import { expandHomeTimeline } from '../../actions/timelines';
import { expandNotifications } from '../../actions/notifications'; import { expandNotifications } from '../../actions/notifications';
import { fetchServer } from '../../actions/server'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
import { clearHeight } from '../../actions/height_cache'; import { clearHeight } from '../../actions/height_cache';
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';
@ -399,6 +399,7 @@ class UI extends React.PureComponent {
this.props.dispatch(fetchMarkers()); this.props.dispatch(fetchMarkers());
this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandHomeTimeline());
this.props.dispatch(expandNotifications()); this.props.dispatch(expandNotifications());
this.props.dispatch(fetchServerTranslationLanguages());
setTimeout(() => this.props.dispatch(fetchServer()), 3000); setTimeout(() => this.props.dispatch(fetchServer()), 3000);
} }
@ -474,10 +475,10 @@ class UI extends React.PureComponent {
}; };
handleHotkeyBack = () => { handleHotkeyBack = () => {
if (window.history && window.history.length === 1) { if (window.history && window.history.state) {
this.context.router.history.push('/');
} else {
this.context.router.history.goBack(); this.context.router.history.goBack();
} else {
this.context.router.history.push('/');
} }
}; };

@ -102,6 +102,7 @@ class Video extends React.PureComponent {
frameRate: PropTypes.string, frameRate: PropTypes.string,
src: PropTypes.string.isRequired, src: PropTypes.string.isRequired,
alt: PropTypes.string, alt: PropTypes.string,
lang: PropTypes.string,
width: PropTypes.number, width: PropTypes.number,
height: PropTypes.number, height: PropTypes.number,
sensitive: PropTypes.bool, sensitive: PropTypes.bool,
@ -524,7 +525,7 @@ class Video extends React.PureComponent {
} }
render () { render () {
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, editable, blurhash, autoFocus } = this.props; const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
const playerStyle = {}; const playerStyle = {};
@ -585,6 +586,7 @@ class Video extends React.PureComponent {
tabIndex='0' tabIndex='0'
aria-label={alt} aria-label={alt}
title={alt} title={alt}
lang={lang}
width={width} width={width}
height={height} height={height}
volume={volume} volume={volume}

@ -162,6 +162,8 @@
"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": "Blokkeer die hele domein", "confirmations.domain_block.confirm": "Blokkeer die hele domein",
"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.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.logout.confirm": "Teken Uit", "confirmations.logout.confirm": "Teken Uit",
"confirmations.logout.message": "Is jy seker jy wil uitteken?", "confirmations.logout.message": "Is jy seker jy wil uitteken?",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Mute",

@ -162,6 +162,8 @@
"confirmations.discard_edit_media.message": "Tiens cambios sin alzar en a descripción u vista previa d'o fichero audiovisual, descartar-los de totz modos?", "confirmations.discard_edit_media.message": "Tiens cambios sin alzar en a descripción u vista previa d'o fichero audiovisual, descartar-los de totz modos?",
"confirmations.domain_block.confirm": "Amagar dominio entero", "confirmations.domain_block.confirm": "Amagar dominio entero",
"confirmations.domain_block.message": "Yes seguro que quiers blocar lo dominio {domain} entero? En cheneral ye prou, y preferible, fer uns quantos bloqueyos y silenciaus concretos. Los tuyos seguidros d'ixe dominio serán eliminaus.", "confirmations.domain_block.message": "Yes seguro que quiers blocar lo dominio {domain} entero? En cheneral ye prou, y preferible, fer uns quantos bloqueyos y silenciaus concretos. Los tuyos seguidros d'ixe dominio serán eliminaus.",
"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.logout.confirm": "Zarrar sesión", "confirmations.logout.confirm": "Zarrar sesión",
"confirmations.logout.message": "Yes seguro de querer zarrar la sesión?", "confirmations.logout.message": "Yes seguro de querer zarrar la sesión?",
"confirmations.mute.confirm": "Silenciar", "confirmations.mute.confirm": "Silenciar",

@ -138,7 +138,7 @@
"compose_form.poll.remove_option": "إزالة هذا الخيار", "compose_form.poll.remove_option": "إزالة هذا الخيار",
"compose_form.poll.switch_to_multiple": "تغيِير الاستطلاع للسماح باِخيارات مُتعدِّدة", "compose_form.poll.switch_to_multiple": "تغيِير الاستطلاع للسماح باِخيارات مُتعدِّدة",
"compose_form.poll.switch_to_single": "تغيِير الاستطلاع للسماح باِخيار واحد فقط", "compose_form.poll.switch_to_single": "تغيِير الاستطلاع للسماح باِخيار واحد فقط",
"compose_form.publish": "انشر", "compose_form.publish": "نشر",
"compose_form.publish_form": "انشر", "compose_form.publish_form": "انشر",
"compose_form.publish_loud": "{publish}!", "compose_form.publish_loud": "{publish}!",
"compose_form.save_changes": "احفظ التعديلات", "compose_form.save_changes": "احفظ التعديلات",
@ -162,6 +162,8 @@
"confirmations.discard_edit_media.message": "لديك تغييرات غير محفوظة لوصف الوسائط أو معاينتها، تجاهلها على أي حال؟", "confirmations.discard_edit_media.message": "لديك تغييرات غير محفوظة لوصف الوسائط أو معاينتها، تجاهلها على أي حال؟",
"confirmations.domain_block.confirm": "حظر اِسم النِّطاق بشكلٍ كامل", "confirmations.domain_block.confirm": "حظر اِسم النِّطاق بشكلٍ كامل",
"confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.", "confirmations.domain_block.message": "متأكد من أنك تود حظر اسم النطاق {domain} بالكامل ؟ في غالب الأحيان يُستَحسَن كتم أو حظر بعض الحسابات بدلا من حظر نطاق بالكامل.\nلن تتمكن مِن رؤية محتوى هذا النطاق لا على خيوطك العمومية و لا في إشعاراتك. سوف يتم كذلك إزالة كافة متابعيك المنتمين إلى هذا النطاق.",
"confirmations.edit.confirm": "تعديل",
"confirmations.edit.message": "التعديل في الحين سوف يُعيد كتابة الرسالة التي أنت بصدد تحريرها. متأكد من أنك تريد المواصلة؟",
"confirmations.logout.confirm": "خروج", "confirmations.logout.confirm": "خروج",
"confirmations.logout.message": "متأكد من أنك تريد الخروج؟", "confirmations.logout.message": "متأكد من أنك تريد الخروج؟",
"confirmations.mute.confirm": "أكتم", "confirmations.mute.confirm": "أكتم",
@ -176,7 +178,7 @@
"conversation.delete": "احذف المحادثة", "conversation.delete": "احذف المحادثة",
"conversation.mark_as_read": "اعتبرها كمقروءة", "conversation.mark_as_read": "اعتبرها كمقروءة",
"conversation.open": "اعرض المحادثة", "conversation.open": "اعرض المحادثة",
"conversation.with": "بـ {names}", "conversation.with": "مع {names}",
"copypaste.copied": "تم نسخه", "copypaste.copied": "تم نسخه",
"copypaste.copy": "انسخ", "copypaste.copy": "انسخ",
"directory.federated": "مِن الفديفرس المعروف", "directory.federated": "مِن الفديفرس المعروف",
@ -215,7 +217,7 @@
"empty_column.bookmarked_statuses": "ليس لديك أية منشورات في الفواصل المرجعية بعد. عندما ستقوم بإضافة البعض منها، ستظهر هنا.", "empty_column.bookmarked_statuses": "ليس لديك أية منشورات في الفواصل المرجعية بعد. عندما ستقوم بإضافة البعض منها، ستظهر هنا.",
"empty_column.community": "الخط العام المحلي فارغ. أكتب شيئا ما للعامة كبداية!", "empty_column.community": "الخط العام المحلي فارغ. أكتب شيئا ما للعامة كبداية!",
"empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.", "empty_column.direct": "لم تتلق أية رسالة خاصة مباشِرة بعد. سوف يتم عرض الرسائل المباشرة هنا إن قمت بإرسال واحدة أو تلقيت البعض منها.",
"empty_column.domain_blocks": "ليس هناك نطاقات مخفية بعد.", "empty_column.domain_blocks": "ليس هناك نطاقات تم حجبها بعد.",
"empty_column.explore_statuses": "ليس هناك ما هو متداوَل الآن. عد في وقت لاحق!", "empty_column.explore_statuses": "ليس هناك ما هو متداوَل الآن. عد في وقت لاحق!",
"empty_column.favourited_statuses": "ليس لديك أية منشورات مفضلة بعد. عندما ستقوم بالإعجاب بواحدة، ستظهر هنا.", "empty_column.favourited_statuses": "ليس لديك أية منشورات مفضلة بعد. عندما ستقوم بالإعجاب بواحدة، ستظهر هنا.",
"empty_column.favourites": "لم يقم أي أحد بالإعجاب بهذا المنشور بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.", "empty_column.favourites": "لم يقم أي أحد بالإعجاب بهذا المنشور بعد. عندما يقوم أحدهم بذلك سوف يظهر هنا.",
@ -245,7 +247,7 @@
"filter_modal.added.context_mismatch_explanation": "فئة عامل التصفية هذه لا تنطبق على السياق الذي وصلت فيه إلى هذه المشاركة. إذا كنت ترغب في تصفية المنشور في هذا السياق أيضا، فسيتعين عليك تعديل عامل التصفية.", "filter_modal.added.context_mismatch_explanation": "فئة عامل التصفية هذه لا تنطبق على السياق الذي وصلت فيه إلى هذه المشاركة. إذا كنت ترغب في تصفية المنشور في هذا السياق أيضا، فسيتعين عليك تعديل عامل التصفية.",
"filter_modal.added.context_mismatch_title": "عدم تطابق السياق!", "filter_modal.added.context_mismatch_title": "عدم تطابق السياق!",
"filter_modal.added.expired_explanation": "انتهت صلاحية فئة عامل التصفية هذه، سوف تحتاج إلى تغيير تاريخ انتهاء الصلاحية لتطبيقها.", "filter_modal.added.expired_explanation": "انتهت صلاحية فئة عامل التصفية هذه، سوف تحتاج إلى تغيير تاريخ انتهاء الصلاحية لتطبيقها.",
"filter_modal.added.expired_title": "تصفية منتهية الصلاحية!", "filter_modal.added.expired_title": "عامل تصفية انتهت صلاحيته!",
"filter_modal.added.review_and_configure": "لمراجعة وزيادة تكوين فئة عوامل التصفية هذه، انتقل إلى {settings_link}.", "filter_modal.added.review_and_configure": "لمراجعة وزيادة تكوين فئة عوامل التصفية هذه، انتقل إلى {settings_link}.",
"filter_modal.added.review_and_configure_title": "إعدادات التصفية", "filter_modal.added.review_and_configure_title": "إعدادات التصفية",
"filter_modal.added.settings_link": "صفحة الإعدادات", "filter_modal.added.settings_link": "صفحة الإعدادات",

@ -38,7 +38,7 @@
"account.follows.empty": "Esti perfil entá nun sigue a naide.", "account.follows.empty": "Esti perfil entá nun sigue a naide.",
"account.follows_you": "Síguete", "account.follows_you": "Síguete",
"account.go_to_profile": "Go to profile", "account.go_to_profile": "Go to profile",
"account.hide_reblogs": "Hide boosts from @{name}", "account.hide_reblogs": "Anubrir los artículos compartíos de @{name}",
"account.joined_short": "Data de xunión", "account.joined_short": "Data de xunión",
"account.languages": "Change subscribed languages", "account.languages": "Change subscribed languages",
"account.link_verified_on": "Ownership of this link was checked on {date}", "account.link_verified_on": "Ownership of this link was checked on {date}",
@ -65,7 +65,7 @@
"account.unfollow": "Dexar de siguir", "account.unfollow": "Dexar de siguir",
"account.unmute": "Activar los avisos de @{name}", "account.unmute": "Activar los avisos de @{name}",
"account.unmute_notifications": "Activar los avisos de @{name}", "account.unmute_notifications": "Activar los avisos de @{name}",
"account.unmute_short": "Unmute", "account.unmute_short": "Activar los avisos",
"account_note.placeholder": "Calca equí p'amestar una nota", "account_note.placeholder": "Calca equí p'amestar una nota",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up",
@ -162,6 +162,8 @@
"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": "Bloquiar tol dominiu", "confirmations.domain_block.confirm": "Bloquiar tol dominiu",
"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.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?",
"confirmations.logout.confirm": "Zarrar la sesión", "confirmations.logout.confirm": "Zarrar la sesión",
"confirmations.logout.message": "¿De xuru que quies zarrar la sesión?", "confirmations.logout.message": "¿De xuru que quies zarrar la sesión?",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Mute",
@ -213,7 +215,7 @@
"empty_column.account_unavailable": "Profile unavailable", "empty_column.account_unavailable": "Profile unavailable",
"empty_column.blocks": "Entá nun bloquiesti a nengún perfil.", "empty_column.blocks": "Entá nun bloquiesti a nengún perfil.",
"empty_column.bookmarked_statuses": "Entá nun tienes nengún artículu en Marcadores. Cuando amiestes dalgún, apaez equí.", "empty_column.bookmarked_statuses": "Entá nun tienes nengún artículu en Marcadores. Cuando amiestes dalgún, apaez equí.",
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!", "empty_column.community": "La llinia de tiempu llocal ta balera. ¡Espubliza daqué públicamente pa comenzar l'alderique!",
"empty_column.direct": "Entá nun tienes nengún mensaxe direutu. Cuando unvies o recibas dalgún, apaez equí.", "empty_column.direct": "Entá nun tienes nengún mensaxe direutu. Cuando unvies o recibas dalgún, apaez equí.",
"empty_column.domain_blocks": "Entá nun hai nengún dominiu bloquiáu.", "empty_column.domain_blocks": "Entá nun hai nengún dominiu bloquiáu.",
"empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!", "empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!",
@ -233,7 +235,7 @@
"error.unexpected_crash.explanation": "Pola mor d'un fallu nel códigu o un problema de compatibilidá del restolador, esta páxina nun se pudo amosar correutamente.", "error.unexpected_crash.explanation": "Pola mor d'un fallu nel códigu o un problema de compatibilidá del restolador, esta páxina nun se pudo amosar correutamente.",
"error.unexpected_crash.explanation_addons": "Esta páxina nun se pudo amosar correutamente. Ye probable que dalgún complementu del restolador o dalguna ferramienta de traducción automática produxere esti error.", "error.unexpected_crash.explanation_addons": "Esta páxina nun se pudo amosar correutamente. Ye probable que dalgún complementu del restolador o dalguna ferramienta de traducción automática produxere esti error.",
"error.unexpected_crash.next_steps": "Prueba a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.", "error.unexpected_crash.next_steps": "Prueba a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.",
"error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "error.unexpected_crash.next_steps_addons": "Prueba a desactivalos ya a anovar la páxina. Si nun sirve, ye posible que tovía seyas a usar Mastodon pente otru restolador o una aplicación nativa.",
"errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard",
"errors.unexpected_crash.report_issue": "Report issue", "errors.unexpected_crash.report_issue": "Report issue",
"explore.search_results": "Resultaos de la busca", "explore.search_results": "Resultaos de la busca",
@ -274,7 +276,7 @@
"footer.source_code": "Ver el códigu fonte", "footer.source_code": "Ver el códigu fonte",
"footer.status": "Estáu", "footer.status": "Estáu",
"generic.saved": "Guardóse", "generic.saved": "Guardóse",
"getting_started.heading": "Getting started", "getting_started.heading": "Comienzu",
"hashtag.column_header.tag_mode.all": "y {additional}", "hashtag.column_header.tag_mode.all": "y {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}", "hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "ensin {additional}", "hashtag.column_header.tag_mode.none": "ensin {additional}",
@ -383,7 +385,7 @@
"navigation_bar.filters": "Pallabres desactivaes", "navigation_bar.filters": "Pallabres desactivaes",
"navigation_bar.follow_requests": "Solicitúes de siguimientu", "navigation_bar.follow_requests": "Solicitúes de siguimientu",
"navigation_bar.followed_tags": "Followed hashtags", "navigation_bar.followed_tags": "Followed hashtags",
"navigation_bar.follows_and_followers": "Follows and followers", "navigation_bar.follows_and_followers": "Perfiles que sigues ya te siguen",
"navigation_bar.lists": "Llistes", "navigation_bar.lists": "Llistes",
"navigation_bar.logout": "Zarrar la sesión", "navigation_bar.logout": "Zarrar la sesión",
"navigation_bar.mutes": "Perfiles colos avisos desactivaos", "navigation_bar.mutes": "Perfiles colos avisos desactivaos",
@ -491,7 +493,7 @@
"report.comment.title": "¿Hai daqué más qu'habríemos saber?", "report.comment.title": "¿Hai daqué más qu'habríemos saber?",
"report.forward": "Reunviar a {target}", "report.forward": "Reunviar a {target}",
"report.forward_hint": "La cuenta ye d'otru sirvidor. ¿Quies unviar a esi sirvidor una copia anónima del informe?", "report.forward_hint": "La cuenta ye d'otru sirvidor. ¿Quies unviar a esi sirvidor una copia anónima del informe?",
"report.mute": "Mute", "report.mute": "Desactivar los avisos",
"report.mute_explanation": "You will not see their posts. They can still follow you and see your posts and will not know that they are muted.", "report.mute_explanation": "You will not see their posts. They can still follow you and see your posts and will not know that they are muted.",
"report.next": "Siguiente", "report.next": "Siguiente",
"report.placeholder": "Comentarios adicionales", "report.placeholder": "Comentarios adicionales",
@ -508,11 +510,11 @@
"report.statuses.subtitle": "Select all that apply", "report.statuses.subtitle": "Select all that apply",
"report.statuses.title": "¿Hai dalgún artículu qu'apoye esti informe?", "report.statuses.title": "¿Hai dalgún artículu qu'apoye esti informe?",
"report.submit": "Unviar", "report.submit": "Unviar",
"report.target": "Report {target}", "report.target": "Informe de: {target}",
"report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:", "report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:",
"report.thanks.take_action_actionable": "While we review this, you can take action against @{name}:", "report.thanks.take_action_actionable": "Mentanto revisamos esti informe, pues tomar midíes contra @{name}:",
"report.thanks.title": "Don't want to see this?", "report.thanks.title": "¿Nun quies ver esti conteníu?",
"report.thanks.title_actionable": "Thanks for reporting, we'll look into this.", "report.thanks.title_actionable": "Gracies pol informe, el casu xá ta n'investigación.",
"report.unfollow": "Dexar de siguir a @{name}", "report.unfollow": "Dexar de siguir a @{name}",
"report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.", "report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.",
"report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} artículu} other {Axuntáronse {count} artículos}}", "report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} artículu} other {Axuntáronse {count} artículos}}",

@ -162,6 +162,8 @@
"confirmations.discard_edit_media.message": "У вас ёсць незахаваныя змены ў апісанні або прэв'ю, усе роўна скасаваць іх?", "confirmations.discard_edit_media.message": "У вас ёсць незахаваныя змены ў апісанні або прэв'ю, усе роўна скасаваць іх?",
"confirmations.domain_block.confirm": "Заблакіраваць дамен цалкам", "confirmations.domain_block.confirm": "Заблакіраваць дамен цалкам",
"confirmations.domain_block.message": "Вы абсалютна дакладна ўпэўнены, што хочаце заблакіраваць {domain} зусім? У большасці выпадкаў, дастаткова некалькіх мэтавых блакіровак ці ігнараванняў. Вы перастанеце бачыць змесціва з гэтага дамену ва ўсіх стужках і апавяшчэннях. Вашы падпіскі з гэтага дамену будуць выдаленыя.", "confirmations.domain_block.message": "Вы абсалютна дакладна ўпэўнены, што хочаце заблакіраваць {domain} зусім? У большасці выпадкаў, дастаткова некалькіх мэтавых блакіровак ці ігнараванняў. Вы перастанеце бачыць змесціва з гэтага дамену ва ўсіх стужках і апавяшчэннях. Вашы падпіскі з гэтага дамену будуць выдаленыя.",
"confirmations.edit.confirm": "Рэдагаваць",
"confirmations.edit.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.logout.confirm": "Выйсці", "confirmations.logout.confirm": "Выйсці",
"confirmations.logout.message": "Вы ўпэўненыя, што хочаце выйсці?", "confirmations.logout.message": "Вы ўпэўненыя, што хочаце выйсці?",
"confirmations.mute.confirm": "Ігнараваць", "confirmations.mute.confirm": "Ігнараваць",
@ -203,7 +205,7 @@
"emoji_button.not_found": "Адпаведныя эмодзі не знойдзены", "emoji_button.not_found": "Адпаведныя эмодзі не знойдзены",
"emoji_button.objects": "Прадметы", "emoji_button.objects": "Прадметы",
"emoji_button.people": "Людзі", "emoji_button.people": "Людзі",
"emoji_button.recent": "Карыстаныя найчасцей", "emoji_button.recent": "Чата выкарыстаныя",
"emoji_button.search": "Пошук...", "emoji_button.search": "Пошук...",
"emoji_button.search_results": "Вынікі пошуку", "emoji_button.search_results": "Вынікі пошуку",
"emoji_button.symbols": "Сімвалы", "emoji_button.symbols": "Сімвалы",
@ -354,10 +356,10 @@
"lists.edit.submit": "Змяніць назву", "lists.edit.submit": "Змяніць назву",
"lists.new.create": "Дадаць спіс", "lists.new.create": "Дадаць спіс",
"lists.new.title_placeholder": "Назва новага спіса", "lists.new.title_placeholder": "Назва новага спіса",
"lists.replies_policy.followed": "Любы карыстальнік, за якім вы падпісаліся", "lists.replies_policy.followed": "Любы карыстальнік, на якога вы падпісаліся",
"lists.replies_policy.list": "Удзельнікі гэтага спісу", "lists.replies_policy.list": "Удзельнікі гэтага спісу",
"lists.replies_policy.none": "Нікога", "lists.replies_policy.none": "Нікога",
"lists.replies_policy.title": "Паказаць адказы да:", "lists.replies_policy.title": "Паказваць адказы:",
"lists.search": "Шукайце сярод людзей, на якіх Вы падпісаны", "lists.search": "Шукайце сярод людзей, на якіх Вы падпісаны",
"lists.subheading": "Вашыя спісы", "lists.subheading": "Вашыя спісы",
"load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}", "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}",

@ -18,7 +18,7 @@
"account.block": "Блокиране на @{name}", "account.block": "Блокиране на @{name}",
"account.block_domain": "Блокиране на домейн {domain}", "account.block_domain": "Блокиране на домейн {domain}",
"account.blocked": "Блокирани", "account.blocked": "Блокирани",
"account.browse_more_on_origin_server": "Разглеждане на още в първообразния профил", "account.browse_more_on_origin_server": "Разглеждане на още в оригиналния профил",
"account.cancel_follow_request": "Оттегляне на заявката за последване", "account.cancel_follow_request": "Оттегляне на заявката за последване",
"account.direct": "Директно съобщение до @{name}", "account.direct": "Директно съобщение до @{name}",
"account.disable_notifications": "Сприране на известия при публикуване от @{name}", "account.disable_notifications": "Сприране на известия при публикуване от @{name}",
@ -162,6 +162,8 @@
"confirmations.discard_edit_media.message": "Не сте запазили промени на описанието или огледа на мултимедията, отхвърляте ли ги?", "confirmations.discard_edit_media.message": "Не сте запазили промени на описанието или огледа на мултимедията, отхвърляте ли ги?",
"confirmations.domain_block.confirm": "Блокиране на целия домейн", "confirmations.domain_block.confirm": "Блокиране на целия домейн",
"confirmations.domain_block.message": "Наистина ли искате да блокирате целия {domain}? В повечето случаи няколко блокирания или заглушавания са достатъчно и за предпочитане. Няма да виждате съдържание от домейна из публични часови оси или известията си. Вашите последователи от този домейн ще се премахнат.", "confirmations.domain_block.message": "Наистина ли искате да блокирате целия {domain}? В повечето случаи няколко блокирания или заглушавания са достатъчно и за предпочитане. Няма да виждате съдържание от домейна из публични часови оси или известията си. Вашите последователи от този домейн ще се премахнат.",
"confirmations.edit.confirm": "Редактиране",
"confirmations.edit.message": "Редактирането сега ще замени съобщението, което в момента съставяте. Сигурни ли сте, че искате да продължите?",
"confirmations.logout.confirm": "Излизане", "confirmations.logout.confirm": "Излизане",
"confirmations.logout.message": "Наистина ли искате да излезете?", "confirmations.logout.message": "Наистина ли искате да излезете?",
"confirmations.mute.confirm": "Заглушаване", "confirmations.mute.confirm": "Заглушаване",
@ -353,7 +355,7 @@
"lists.edit": "Промяна на списъка", "lists.edit": "Промяна на списъка",
"lists.edit.submit": "Промяна на заглавие", "lists.edit.submit": "Промяна на заглавие",
"lists.new.create": "Добавяне на списък", "lists.new.create": "Добавяне на списък",
"lists.new.title_placeholder": "Име на нов списък", "lists.new.title_placeholder": "Ново заглавие на списъка",
"lists.replies_policy.followed": "Някой последван потребител", "lists.replies_policy.followed": "Някой последван потребител",
"lists.replies_policy.list": "Членуващите в списъка", "lists.replies_policy.list": "Членуващите в списъка",
"lists.replies_policy.none": "Никого", "lists.replies_policy.none": "Никого",

@ -162,6 +162,8 @@
"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": "এই ডোমেন থেকে সব লুকান", "confirmations.domain_block.confirm": "এই ডোমেন থেকে সব লুকান",
"confirmations.domain_block.message": "আপনি কি সত্যিই সত্যই নিশ্চিত যে আপনি পুরো {domain}'টি ব্লক করতে চান? বেশিরভাগ ক্ষেত্রে কয়েকটি লক্ষ্যযুক্ত ব্লক বা নীরবতা যথেষ্ট এবং পছন্দসই। আপনি কোনও পাবলিক টাইমলাইন বা আপনার বিজ্ঞপ্তিগুলিতে সেই ডোমেন থেকে সামগ্রী দেখতে পাবেন না। সেই ডোমেন থেকে আপনার অনুসরণকারীদের সরানো হবে।", "confirmations.domain_block.message": "আপনি কি সত্যিই সত্যই নিশ্চিত যে আপনি পুরো {domain}'টি ব্লক করতে চান? বেশিরভাগ ক্ষেত্রে কয়েকটি লক্ষ্যযুক্ত ব্লক বা নীরবতা যথেষ্ট এবং পছন্দসই। আপনি কোনও পাবলিক টাইমলাইন বা আপনার বিজ্ঞপ্তিগুলিতে সেই ডোমেন থেকে সামগ্রী দেখতে পাবেন না। সেই ডোমেন থেকে আপনার অনুসরণকারীদের সরানো হবে।",
"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.logout.confirm": "প্রস্থান", "confirmations.logout.confirm": "প্রস্থান",
"confirmations.logout.message": "আপনি লগ আউট করতে চান?", "confirmations.logout.message": "আপনি লগ আউট করতে চান?",
"confirmations.mute.confirm": "সরিয়ে ফেলুন", "confirmations.mute.confirm": "সরিয়ে ফেলুন",

@ -162,6 +162,8 @@
"confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?", "confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?",
"confirmations.domain_block.confirm": "Berzañ an domani a-bezh", "confirmations.domain_block.confirm": "Berzañ an domani a-bezh",
"confirmations.domain_block.message": "Ha sur oc'h e fell deoc'h berzañ an {domain} a-bezh? Peurvuiañ eo trawalc'h berzañ pe mudañ un nebeud implijer·ezed·ien. Ne welot danvez ebet o tont eus an domani-mañ. Dilamet e vo ar c'houmanantoù war an domani-mañ.", "confirmations.domain_block.message": "Ha sur oc'h e fell deoc'h berzañ an {domain} a-bezh? Peurvuiañ eo trawalc'h berzañ pe mudañ un nebeud implijer·ezed·ien. Ne welot danvez ebet o tont eus an domani-mañ. Dilamet e vo ar c'houmanantoù war an domani-mañ.",
"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.logout.confirm": "Digevreañ", "confirmations.logout.confirm": "Digevreañ",
"confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?", "confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?",
"confirmations.mute.confirm": "Kuzhat", "confirmations.mute.confirm": "Kuzhat",

@ -162,6 +162,8 @@
"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": "Hide entire domain", "confirmations.domain_block.confirm": "Hide entire domain",
"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.message": "Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?",
"confirmations.logout.confirm": "Log out", "confirmations.logout.confirm": "Log out",
"confirmations.logout.message": "Are you sure you want to log out?", "confirmations.logout.message": "Are you sure you want to log out?",
"confirmations.mute.confirm": "Mute", "confirmations.mute.confirm": "Mute",

@ -162,6 +162,8 @@
"confirmations.discard_edit_media.message": "Tens canvis no desats en la descripció del contingut o en la previsualització, els vols descartar?", "confirmations.discard_edit_media.message": "Tens canvis no desats en la descripció del contingut o en la previsualització, els vols descartar?",
"confirmations.domain_block.confirm": "Bloca el domini sencer", "confirmations.domain_block.confirm": "Bloca el domini sencer",
"confirmations.domain_block.message": "Segur que vols blocar {domain} del tot? En la majoria dels casos, només amb blocar o silenciar uns pocs comptes n'hi ha prou i és millor. No veuràs el contingut daquest domini en cap de les línies de temps ni en les notificacions. S'eliminaran els teus seguidors daquest domini.", "confirmations.domain_block.message": "Segur que vols blocar {domain} del tot? En la majoria dels casos, només amb blocar o silenciar uns pocs comptes n'hi ha prou i és millor. No veuràs el contingut daquest domini en cap de les línies de temps ni en les notificacions. S'eliminaran els teus seguidors daquest domini.",
"confirmations.edit.confirm": "Edita",
"confirmations.edit.message": "Editant ara sobreescriuràs el missatge que estàs editant. Segur que vols continuar?",
"confirmations.logout.confirm": "Tanca la sessió", "confirmations.logout.confirm": "Tanca la sessió",
"confirmations.logout.message": "Segur que vols tancar la sessió?", "confirmations.logout.message": "Segur que vols tancar la sessió?",
"confirmations.mute.confirm": "Silencia", "confirmations.mute.confirm": "Silencia",
@ -437,7 +439,7 @@
"notifications.group": "{count} notificacions", "notifications.group": "{count} notificacions",
"notifications.mark_as_read": "Marca cada notificació com a llegida", "notifications.mark_as_read": "Marca cada notificació com a llegida",
"notifications.permission_denied": "Les notificacions descriptori no estan disponibles perquè prèviament sha denegat el permís al navegador", "notifications.permission_denied": "Les notificacions descriptori no estan disponibles perquè prèviament sha denegat el permís al navegador",
"notifications.permission_denied_alert": "No es poden activar les notificacions de l'escriptori perquè el permís del navegador ha estat denegat abans", "notifications.permission_denied_alert": "No es poden activar les notificacions de l'escriptori perquè abans s'ha denegat el permís del navegador",
"notifications.permission_required": "Les notificacions d'escriptori no estan disponibles perquè el permís requerit no ha estat concedit.", "notifications.permission_required": "Les notificacions d'escriptori no estan disponibles perquè el permís requerit no ha estat concedit.",
"notifications_permission_banner.enable": "Activa les notificacions descriptori", "notifications_permission_banner.enable": "Activa les notificacions descriptori",
"notifications_permission_banner.how_to_control": "Per a rebre notificacions quan Mastodon no és obert cal activar les notificacions descriptori. Pots controlar amb precisió quins tipus dinteraccions generen notificacions descriptori després dactivar el botó {icon} de dalt.", "notifications_permission_banner.how_to_control": "Per a rebre notificacions quan Mastodon no és obert cal activar les notificacions descriptori. Pots controlar amb precisió quins tipus dinteraccions generen notificacions descriptori després dactivar el botó {icon} de dalt.",

@ -54,7 +54,7 @@
"account.posts_with_replies": "توتس و وەڵامەکان", "account.posts_with_replies": "توتس و وەڵامەکان",
"account.report": "گوزارشت @{name}", "account.report": "گوزارشت @{name}",
"account.requested": "چاوەڕێی ڕەزامەندین. کرتە بکە بۆ هەڵوەشاندنەوەی داواکاری شوێنکەوتن", "account.requested": "چاوەڕێی ڕەزامەندین. کرتە بکە بۆ هەڵوەشاندنەوەی داواکاری شوێنکەوتن",
"account.requested_follow": "{name} has requested to follow you", "account.requested_follow": "{name} داوای کردووە شوێنت بکەوێت",
"account.share": "پرۆفایلی @{name} هاوبەش بکە", "account.share": "پرۆفایلی @{name} هاوبەش بکە",
"account.show_reblogs": "پیشاندانی بەرزکردنەوەکان لە @{name}", "account.show_reblogs": "پیشاندانی بەرزکردنەوەکان لە @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}", "account.statuses_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}",
@ -128,7 +128,7 @@
"compose.language.search": "گەڕان بە زمانەکان...", "compose.language.search": "گەڕان بە زمانەکان...",
"compose_form.direct_message_warning_learn_more": "زیاتر فێربه", "compose_form.direct_message_warning_learn_more": "زیاتر فێربه",
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.", "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", "compose_form.hashtag_warning": "ئەم بڵاوکراوەیە لە ژێر هیچ هاشتاگێکدا دا نانرێت وەک ئەوەیە، کە گشتی نەبێت. تەنها بڵاوکراوە گشتیەکان دەتوانرێ بە هاشتاگ گەڕانی بۆ بکرێت.",
"compose_form.lock_disclaimer": "هەژمێرەکەی لە حاڵەتی {locked}. هەر کەسێک دەتوانێت شوێنت بکەوێت بۆ پیشاندانی بابەتەکانی تەنها دوایخۆی.", "compose_form.lock_disclaimer": "هەژمێرەکەی لە حاڵەتی {locked}. هەر کەسێک دەتوانێت شوێنت بکەوێت بۆ پیشاندانی بابەتەکانی تەنها دوایخۆی.",
"compose_form.lock_disclaimer.lock": "قفڵ دراوە", "compose_form.lock_disclaimer.lock": "قفڵ دراوە",
"compose_form.placeholder": "چی لە مێشکتدایە?", "compose_form.placeholder": "چی لە مێشکتدایە?",
@ -162,6 +162,8 @@
"confirmations.discard_edit_media.message": "گۆڕانکاریت لە وەسف یان پێشبینی میدیادا هەڵنەگیراوە، بەهەر حاڵ فڕێیان بدە؟", "confirmations.discard_edit_media.message": "گۆڕانکاریت لە وەسف یان پێشبینی میدیادا هەڵنەگیراوە، بەهەر حاڵ فڕێیان بدە؟",
"confirmations.domain_block.confirm": "بلۆککردنی هەموو دۆمەینەکە", "confirmations.domain_block.confirm": "بلۆککردنی هەموو دۆمەینەکە",
"confirmations.domain_block.message": "ئایا بەڕاستی، بەڕاستی تۆ دەتەوێت هەموو {domain} بلۆک بکەیت؟ لە زۆربەی حاڵەتەکاندا چەند بلۆکێکی ئامانجدار یان بێدەنگەکان پێویست و پەسەندن. تۆ ناوەڕۆک ێک نابینیت لە دۆمەینەکە لە هیچ هێڵی کاتی گشتی یان ئاگانامەکانت. شوێنکەوتوانی تۆ لەو دۆمەینەوە لادەبرێن.", "confirmations.domain_block.message": "ئایا بەڕاستی، بەڕاستی تۆ دەتەوێت هەموو {domain} بلۆک بکەیت؟ لە زۆربەی حاڵەتەکاندا چەند بلۆکێکی ئامانجدار یان بێدەنگەکان پێویست و پەسەندن. تۆ ناوەڕۆک ێک نابینیت لە دۆمەینەکە لە هیچ هێڵی کاتی گشتی یان ئاگانامەکانت. شوێنکەوتوانی تۆ لەو دۆمەینەوە لادەبرێن.",
"confirmations.edit.confirm": "دەستکاری",
"confirmations.edit.message": "دەستکاری کردنی ئێستا: دەبێتە هۆی نووسینەوەی ئەو پەیامەی، کە ئێستا داتدەڕشت. ئایا دڵنیای، کە دەتەوێت بەردەوام بیت؟",
"confirmations.logout.confirm": "چوونە دەرەوە", "confirmations.logout.confirm": "چوونە دەرەوە",
"confirmations.logout.message": "ئایا دڵنیایت لەوەی دەتەوێت بچیتە دەرەوە?", "confirmations.logout.message": "ئایا دڵنیایت لەوەی دەتەوێت بچیتە دەرەوە?",
"confirmations.mute.confirm": "بێدەنگ", "confirmations.mute.confirm": "بێدەنگ",
@ -221,7 +223,7 @@
"empty_column.favourites": "کەس ئەم توتەی دڵخواز نەکردووە،کاتێک کەسێک وا بکات، لێرە دەرئەکەون.", "empty_column.favourites": "کەس ئەم توتەی دڵخواز نەکردووە،کاتێک کەسێک وا بکات، لێرە دەرئەکەون.",
"empty_column.follow_recommendations": "پێدەچێت هیچ پێشنیارێک بۆ تۆ دروست نەکرێت. دەتوانیت هەوڵبدەیت گەڕان بەکاربهێنیت بۆ گەڕان بەدوای ئەو کەسانەی کە ڕەنگە بیانناسیت یان بەدوای هاشتاگە ڕەوتەکاندا بگەڕێیت.", "empty_column.follow_recommendations": "پێدەچێت هیچ پێشنیارێک بۆ تۆ دروست نەکرێت. دەتوانیت هەوڵبدەیت گەڕان بەکاربهێنیت بۆ گەڕان بەدوای ئەو کەسانەی کە ڕەنگە بیانناسیت یان بەدوای هاشتاگە ڕەوتەکاندا بگەڕێیت.",
"empty_column.follow_requests": "تۆ هێشتا هیچ داواکارییەکی بەدواداچووت نیە. کاتێک یەکێکت بۆ هات، لێرە دەرئەکەویت.", "empty_column.follow_requests": "تۆ هێشتا هیچ داواکارییەکی بەدواداچووت نیە. کاتێک یەکێکت بۆ هات، لێرە دەرئەکەویت.",
"empty_column.followed_tags": "You have not followed any hashtags yet. When you do, they will show up here.", "empty_column.followed_tags": "تۆ هێشتا شوێن هیچ هاشتاگێک نەکەوتوویت. کاتێک کردت، ئەوان لێرە دەردەکەون.",
"empty_column.hashtag": "هێشتا هیچ شتێک لەم هاشتاگەدا نییە.", "empty_column.hashtag": "هێشتا هیچ شتێک لەم هاشتاگەدا نییە.",
"empty_column.home": "تایم لاینی ماڵەوەت بەتاڵە! سەردانی {public} بکە یان گەڕان بەکاربێنە بۆ دەستپێکردن و بینینی بەکارهێنەرانی تر.", "empty_column.home": "تایم لاینی ماڵەوەت بەتاڵە! سەردانی {public} بکە یان گەڕان بەکاربێنە بۆ دەستپێکردن و بینینی بەکارهێنەرانی تر.",
"empty_column.home.suggestions": "چەند پێشنیارێک ببینە", "empty_column.home.suggestions": "چەند پێشنیارێک ببینە",
@ -237,42 +239,42 @@
"errors.unexpected_crash.copy_stacktrace": "کۆپیکردنی ستێکتراسی بۆ کلیپ بۆرد", "errors.unexpected_crash.copy_stacktrace": "کۆپیکردنی ستێکتراسی بۆ کلیپ بۆرد",
"errors.unexpected_crash.report_issue": "کێشەی گوزارشت", "errors.unexpected_crash.report_issue": "کێشەی گوزارشت",
"explore.search_results": "ئەنجامەکانی گەڕان", "explore.search_results": "ئەنجامەکانی گەڕان",
"explore.suggested_follows": "For you", "explore.suggested_follows": "بۆ تۆ",
"explore.title": "گەڕان", "explore.title": "گەڕان",
"explore.trending_links": "News", "explore.trending_links": "هەواڵەکان",
"explore.trending_statuses": "Posts", "explore.trending_statuses": "بڵاوکراوەکان",
"explore.trending_tags": "Hashtags", "explore.trending_tags": "هاشتاگەکان",
"filter_modal.added.context_mismatch_explanation": "ئەم پۆلە فلتەرە ئەو چوارچێوەیە ناگرێتەوە کە تۆ تێیدا دەستت بەم پۆستە کردووە. ئەگەر بتەوێت پۆستەکە لەم چوارچێوەیەشدا فلتەر بکرێت، دەبێت دەستکاری فلتەرەکە بکەیت.", "filter_modal.added.context_mismatch_explanation": "ئەم پۆلە فلتەرە ئەو چوارچێوەیە ناگرێتەوە کە تۆ تێیدا دەستت بەم پۆستە کردووە. ئەگەر بتەوێت پۆستەکە لەم چوارچێوەیەشدا فلتەر بکرێت، دەبێت دەستکاری فلتەرەکە بکەیت.",
"filter_modal.added.context_mismatch_title": "ناتەبایی دەقی نووسراو!", "filter_modal.added.context_mismatch_title": "ناتەبایی دەقی نووسراو!",
"filter_modal.added.expired_explanation": "ئەم پۆلە فلتەرە بەسەرچووە، پێویستە بەرواری بەسەرچوونی بگۆڕیت بۆ ئەوەی جێبەجێی بکات.", "filter_modal.added.expired_explanation": "ئەم پۆلە فلتەرە بەسەرچووە، پێویستە بەرواری بەسەرچوونی بگۆڕیت بۆ ئەوەی جێبەجێی بکات.",
"filter_modal.added.expired_title": "فلتەری بەسەرچووە!", "filter_modal.added.expired_title": "فلتەری بەسەرچووە!",
"filter_modal.added.review_and_configure": "بۆ پێداچوونەوە و ڕێکخستنی زیاتری ئەم پۆلە فلتەرە، بچۆ بۆ {settings_link}.", "filter_modal.added.review_and_configure": "بۆ پێداچوونەوە و ڕێکخستنی زیاتری ئەم پۆلە فلتەرە، بچۆ بۆ {settings_link}.",
"filter_modal.added.review_and_configure_title": "Filter settings", "filter_modal.added.review_and_configure_title": "ڕێکخستنەکانی پاڵاوتن",
"filter_modal.added.settings_link": "settings page", "filter_modal.added.settings_link": "پەڕەی ڕێکخستن",
"filter_modal.added.short_explanation": "This post has been added to the following filter category: {title}.", "filter_modal.added.short_explanation": "بڵاوکراوەکە زیادکرا بۆ ئەو پاڵاوتنانەی خوارەوە: {title}.",
"filter_modal.added.title": "Filter added!", "filter_modal.added.title": "پاڵێو زیادکرا!",
"filter_modal.select_filter.context_mismatch": "does not apply to this context", "filter_modal.select_filter.context_mismatch": "لەم چوارچێوەیەدا کارپێکراو نییە",
"filter_modal.select_filter.expired": "expired", "filter_modal.select_filter.expired": "بەسەرچووە",
"filter_modal.select_filter.prompt_new": "New category: {name}", "filter_modal.select_filter.prompt_new": "پۆلێنی نوێ: {name}",
"filter_modal.select_filter.search": "Search or create", "filter_modal.select_filter.search": "گەڕان یان دروستکردن",
"filter_modal.select_filter.subtitle": "Use an existing category or create a new one", "filter_modal.select_filter.subtitle": "بەکارهێنانی پۆلێنی بەردەست یان دروستکردنی پۆلێنێکی نوێ",
"filter_modal.select_filter.title": "Filter this post", "filter_modal.select_filter.title": "ئەم بڵاوکراوەیە بپاڵێوە",
"filter_modal.title.status": "Filter a post", "filter_modal.title.status": "بڵاوکراوەیەک بپاڵێوە",
"follow_recommendations.done": "تەواو", "follow_recommendations.done": "تەواو",
"follow_recommendations.heading": "شوێن ئەو کەسانە بکەون کە دەتەوێت پۆستەکان ببینیت لە! لێرەدا چەند پێشنیارێک هەیە.", "follow_recommendations.heading": "شوێن ئەو کەسانە بکەون کە دەتەوێت پۆستەکان ببینیت لە! لێرەدا چەند پێشنیارێک هەیە.",
"follow_recommendations.lead": "بابەتەکانی ئەو کەسانەی کە بەدوایدا دەگەڕێیت بە فەرمانی کرۆنۆلۆجی لە خواردنەکانی ماڵەکەت دەردەکەون. مەترسە لە هەڵەکردن، دەتوانیت بە ئاسانی خەڵک هەڵبکەیت هەر کاتێک!", "follow_recommendations.lead": "بابەتەکانی ئەو کەسانەی کە بەدوایدا دەگەڕێیت بە فەرمانی کرۆنۆلۆجی لە خواردنەکانی ماڵەکەت دەردەکەون. مەترسە لە هەڵەکردن، دەتوانیت بە ئاسانی خەڵک هەڵبکەیت هەر کاتێک!",
"follow_request.authorize": "ده‌سه‌ڵاتپێدراو", "follow_request.authorize": "ده‌سه‌ڵاتپێدراو",
"follow_request.reject": "ڕەتکردنەوە", "follow_request.reject": "ڕەتکردنەوە",
"follow_requests.unlocked_explanation": "هەرچەندە هەژمارەکەت داخراو نییە، ستافی {domain} وا بیریان کردەوە کە لەوانەیە بتانەوێت پێداچوونەوە بە داواکاریەکانی ئەم هەژمارەدا بکەن بە دەستی.", "follow_requests.unlocked_explanation": "هەرچەندە هەژمارەکەت داخراو نییە، ستافی {domain} وا بیریان کردەوە کە لەوانەیە بتانەوێت پێداچوونەوە بە داواکاریەکانی ئەم هەژمارەدا بکەن بە دەستی.",
"followed_tags": "Followed hashtags", "followed_tags": "هاشتاگە شوێنکەوتووەکان",
"footer.about": "About", "footer.about": "دەربارە",
"footer.directory": "Profiles directory", "footer.directory": "ڕابەری پەڕەی ناساندن",
"footer.get_app": "Get the app", "footer.get_app": "بەرنامەکە بەدەست بێنە",
"footer.invite": "Invite people", "footer.invite": "بانگهێشتکردنی خەڵک",
"footer.keyboard_shortcuts": "Keyboard shortcuts", "footer.keyboard_shortcuts": "کورتەڕێکانی تەختەکلیک",
"footer.privacy_policy": "Privacy policy", "footer.privacy_policy": "سیاسەتی تایبەتمەندێتی",
"footer.source_code": "View source code", "footer.source_code": "پیشاندانی کۆدی سەرچاوە",
"footer.status": "Status", "footer.status": "دۆخ",
"generic.saved": "پاشکەوتکرا", "generic.saved": "پاشکەوتکرا",
"getting_started.heading": "دەست پێکردن", "getting_started.heading": "دەست پێکردن",
"hashtag.column_header.tag_mode.all": "و {additional}", "hashtag.column_header.tag_mode.all": "و {additional}",
@ -284,19 +286,19 @@
"hashtag.column_settings.tag_mode.any": "هەر کام لەمانە", "hashtag.column_settings.tag_mode.any": "هەر کام لەمانە",
"hashtag.column_settings.tag_mode.none": "هیچ کام لەمانە", "hashtag.column_settings.tag_mode.none": "هیچ کام لەمانە",
"hashtag.column_settings.tag_toggle": "تاگی زیادە ی ئەم ستوونە لەخۆ بنووسە", "hashtag.column_settings.tag_toggle": "تاگی زیادە ی ئەم ستوونە لەخۆ بنووسە",
"hashtag.follow": "Follow hashtag", "hashtag.follow": "شوێنکەوتنی هاشتاگ",
"hashtag.unfollow": "Unfollow hashtag", "hashtag.unfollow": "شوێن نەکەوتنی هاشتاگ",
"home.column_settings.basic": "بنەڕەتی", "home.column_settings.basic": "بنەڕەتی",
"home.column_settings.show_reblogs": "پیشاندانی بەهێزکردن", "home.column_settings.show_reblogs": "پیشاندانی بەهێزکردن",
"home.column_settings.show_replies": "وەڵامدانەوەکان پیشان بدە", "home.column_settings.show_replies": "وەڵامدانەوەکان پیشان بدە",
"home.hide_announcements": "شاردنەوەی راگەیەنراوەکان", "home.hide_announcements": "شاردنەوەی راگەیەنراوەکان",
"home.show_announcements": "پیشاندانی راگەیەنراوەکان", "home.show_announcements": "پیشاندانی راگەیەنراوەکان",
"interaction_modal.description.favourite": "With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.", "interaction_modal.description.favourite": "بە هەژمارێک لەسەر ماستدۆن، دەتوانیت ئەم بڵاوکراوەیە زیادبکەیت بۆ دڵخوازەکانت. بۆ ئاگادارکردنەوەی بڵاوکەرەوەکە لە پێزانینەکەت و هێشتنەوەی بۆ دواتر.",
"interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.", "interaction_modal.description.follow": "بە هەژمارێک لەسەر ماستدۆن، ئەتوانیت شوێن {name} بکەویت بۆ ئەوەی بڵاوکراوەکانی بگاتە پەڕەی سەرەکیت.",
"interaction_modal.description.reblog": "With an account on Mastodon, you can boost this post to share it with your own followers.", "interaction_modal.description.reblog": "بە هەژمارێک لەسەر ماستدۆن، ئەتوانیت ئەم بڵاوکراوەیە بەرزبکەیتەوە تاوەکو بەژداری پێبکەیت لەگەل شوێنکەوتوانت.",
"interaction_modal.description.reply": "With an account on Mastodon, you can respond to this post.", "interaction_modal.description.reply": "بە هەژمارێک لەسەر ماستدۆن، ئەتوانیت وەڵامی ئەم بڵاوکراوەیە بدەیتەوە.",
"interaction_modal.on_another_server": "On a different server", "interaction_modal.on_another_server": "لەسەر ڕاژەیەکی جیا",
"interaction_modal.on_this_server": "On this server", "interaction_modal.on_this_server": "لەسەر ئەم ڕاژەیە",
"interaction_modal.other_server_instructions": "Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.", "interaction_modal.other_server_instructions": "Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.",
"interaction_modal.preamble": "Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one.", "interaction_modal.preamble": "Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one.",
"interaction_modal.title.favourite": "Favourite {name}'s post", "interaction_modal.title.favourite": "Favourite {name}'s post",

@ -162,6 +162,8 @@
"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": "Piattà tuttu u duminiu", "confirmations.domain_block.confirm": "Piattà tuttu u duminiu",
"confirmations.domain_block.message": "Site veramente sicuru·a che vulete piattà tuttu à {domain}? Saria forse abbastanza di bluccà ò piattà alcuni conti da quallà. Ùn viderete più nunda da quallà indè e linee pubbliche o e nutificazione. I vostri abbunati da stu duminiu saranu tolti.", "confirmations.domain_block.message": "Site veramente sicuru·a che vulete piattà tuttu à {domain}? Saria forse abbastanza di bluccà ò piattà alcuni conti da quallà. Ùn viderete più nunda da quallà indè e linee pubbliche o e nutificazione. I vostri abbunati da stu duminiu saranu tolti.",
"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.logout.confirm": "Scunnettassi", "confirmations.logout.confirm": "Scunnettassi",
"confirmations.logout.message": "Site sicuru·a che vulete scunnettà vi?", "confirmations.logout.message": "Site sicuru·a che vulete scunnettà vi?",
"confirmations.mute.confirm": "Piattà", "confirmations.mute.confirm": "Piattà",

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

Loading…
Cancel
Save