Merge remote-tracking branch 'upstream/main'
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Dalite 2024-06-15 18:21:04 +02:00
commit 6f73d7eedd
295 changed files with 4425 additions and 2546 deletions

View file

@ -1,6 +0,0 @@
---
ignore:
# devise-two-factor advisory about brute-forcing TOTP
# We have rate-limits on authentication endpoints in place (including second
# factor verification) since Mastodon v3.2.0
- CVE-2024-0227

View file

@ -1,20 +1,15 @@
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby # For details, see https://github.com/devcontainers/images/tree/main/src/ruby
FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye FROM mcr.microsoft.com/devcontainers/ruby:1-3.3-bookworm
# Install Rails # Install node version from .nvmrc
# RUN gem install rails webdrivers WORKDIR /app
COPY .nvmrc .
RUN /bin/bash --login -i -c "nvm install"
ARG NODE_VERSION="20" # Install additional OS packages
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" RUN apt-get update && \
export DEBIAN_FRONTEND=noninteractive && \
apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev
# [Optional] Uncomment this section to install additional OS packages. # Move welcome message to where VS Code expects it
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ COPY .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt
&& apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libpam-dev
# [Optional] Uncomment this line to install additional gems.
RUN gem install foreman
# [Optional] Uncomment this line to install global node packages.
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && corepack enable" 2>&1
COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt

View file

@ -1,6 +1,6 @@
{ {
"name": "Mastodon on GitHub Codespaces", "name": "Mastodon on GitHub Codespaces",
"dockerComposeFile": "../docker-compose.yml", "dockerComposeFile": "../compose.yaml",
"service": "app", "service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
@ -23,6 +23,8 @@
} }
}, },
"remoteUser": "root",
"otherPortsAttributes": { "otherPortsAttributes": {
"onAutoForward": "silent" "onAutoForward": "silent"
}, },
@ -37,7 +39,7 @@
}, },
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"postCreateCommand": ".devcontainer/post-create.sh", "postCreateCommand": "bin/setup",
"waitFor": "postCreateCommand", "waitFor": "postCreateCommand",
"customizations": { "customizations": {

View file

@ -1,13 +1,11 @@
version: '3'
services: services:
app: app:
working_dir: /workspaces/mastodon/ working_dir: /workspaces/mastodon/
build: build:
context: . context: ..
dockerfile: Dockerfile dockerfile: .devcontainer/Dockerfile
volumes: volumes:
- ../..:/workspaces:cached - ..:/workspaces/mastodon:cached
environment: environment:
RAILS_ENV: development RAILS_ENV: development
NODE_ENV: development NODE_ENV: development

View file

@ -1,6 +1,6 @@
{ {
"name": "Mastodon on local machine", "name": "Mastodon on local machine",
"dockerComposeFile": "docker-compose.yml", "dockerComposeFile": "compose.yaml",
"service": "app", "service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
@ -23,12 +23,14 @@
} }
}, },
"remoteUser": "root",
"otherPortsAttributes": { "otherPortsAttributes": {
"onAutoForward": "silent" "onAutoForward": "silent"
}, },
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"postCreateCommand": ".devcontainer/post-create.sh", "postCreateCommand": "bin/setup",
"waitFor": "postCreateCommand", "waitFor": "postCreateCommand",
"customizations": { "customizations": {

View file

@ -1,27 +0,0 @@
#!/bin/bash
set -e # Fail the whole script on first error
# Fetch Ruby gem dependencies
bundle config path 'vendor/bundle'
bundle config with 'development test'
bundle install
# Make Gemfile.lock pristine again
git checkout -- Gemfile.lock
# Fetch Javascript dependencies
corepack prepare
yarn install --immutable
# [re]create, migrate, and seed the test database
RAILS_ENV=test ./bin/rails db:setup
# [re]create, migrate, and seed the development database
RAILS_ENV=development ./bin/rails db:setup
# Precompile assets for development
RAILS_ENV=development ./bin/rails assets:precompile
# Precompile assets for test
RAILS_ENV=test ./bin/rails assets:precompile

View file

@ -1,8 +1,7 @@
👋 Welcome to "Mastodon" in GitHub Codespaces! 👋 Welcome to your Mastodon Dev Container!
🛠️ Your environment is fully setup with all the required software. 🛠️ 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). 💥 Run `bin/dev` to start the application processes.
📝 Edit away, run your app as usual, and we'll automatically make it available for you to access.
🥼 Run `RAILS_ENV=test bin/rails assets:precompile && RAILS_ENV=test bin/rspec` to run the test suite.

View file

@ -349,6 +349,9 @@ module.exports = defineConfig({
// Disable formatting rules that have been enabled in the base config // Disable formatting rules that have been enabled in the base config
'indent': 'off', 'indent': 'off',
// This is not needed as we use noImplicitReturns, which handles this in addition to understanding types
'consistent-return': 'off',
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],

View file

@ -14,7 +14,7 @@ runs:
shell: bash shell: bash
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev ${{ inputs.additional-system-dependencies }} sudo apt-get install -y libicu-dev libidn11-dev libvips42 ${{ inputs.additional-system-dependencies }}
- name: Set up Ruby - name: Set up Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1

4
.github/codecov.yml vendored
View file

@ -3,9 +3,9 @@ coverage:
status: status:
project: project:
default: default:
# Github status check is not blocking # GitHub status check is not blocking
informational: true informational: true
patch: patch:
default: default:
# Github status check is not blocking # GitHub status check is not blocking
informational: true informational: true

View file

@ -2,6 +2,7 @@
$schema: 'https://docs.renovatebot.com/renovate-schema.json', $schema: 'https://docs.renovatebot.com/renovate-schema.json',
extends: [ extends: [
'config:recommended', 'config:recommended',
'customManagers:dockerfileVersions',
':labels(dependencies)', ':labels(dependencies)',
':prConcurrentLimitNone', // Remove limit for open PRs at any time. ':prConcurrentLimitNone', // Remove limit for open PRs at any time.
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour. ':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
@ -59,7 +60,7 @@
dependencyDashboardApproval: true, dependencyDashboardApproval: true,
}, },
{ {
// Update Github Actions and Docker images weekly // Update GitHub Actions and Docker images weekly
matchManagers: ['github-actions', 'dockerfile', 'docker-compose'], matchManagers: ['github-actions', 'dockerfile', 'docker-compose'],
extends: ['schedule:weekly'], extends: ['schedule:weekly'],
}, },

View file

@ -68,7 +68,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the Github Container registry - name: Log in to the GitHub Container registry
if: contains(inputs.push_to_images, 'ghcr.io') if: contains(inputs.push_to_images, 'ghcr.io')
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:

View file

@ -6,14 +6,12 @@ on:
paths: paths:
- 'Gemfile*' - 'Gemfile*'
- '.ruby-version' - '.ruby-version'
- '.bundler-audit.yml'
- '.github/workflows/bundler-audit.yml' - '.github/workflows/bundler-audit.yml'
pull_request: pull_request:
paths: paths:
- 'Gemfile*' - 'Gemfile*'
- '.ruby-version' - '.ruby-version'
- '.bundler-audit.yml'
- '.github/workflows/bundler-audit.yml' - '.github/workflows/bundler-audit.yml'
schedule: schedule:
@ -23,12 +21,17 @@ jobs:
security: security:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
BUNDLE_ONLY: development
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Ruby environment - name: Set up Ruby
uses: ./.github/actions/setup-ruby uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Run bundler-audit - name: Run bundler-audit
run: bundle exec bundler-audit run: bundle exec bundler-audit check --update

View file

@ -58,13 +58,13 @@ jobs:
title: 'New Crowdin Translations (automated)' title: 'New Crowdin Translations (automated)'
author: 'GitHub Actions <noreply@github.com>' author: 'GitHub Actions <noreply@github.com>'
body: | body: |
New Crowdin translations, automated with Github Actions New Crowdin translations, automated with GitHub Actions
See `.github/workflows/crowdin-download.yml` See `.github/workflows/crowdin-download.yml`
This PR will be updated every day with new translations. This PR will be updated every day with new translations.
Due to a limitation in Github Actions, checks are not running on this PR without manual action. Due to a limitation in GitHub Actions, checks are not running on this PR without manual action.
If you want to run the checks, then close and re-open it. If you want to run the checks, then close and re-open it.
branch: i18n/crowdin/translations branch: i18n/crowdin/translations
base: main base: main

View file

@ -26,12 +26,18 @@ on:
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
BUNDLE_ONLY: development
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Ruby environment - name: Set up Ruby
uses: ./.github/actions/setup-ruby uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Run haml-lint - name: Run haml-lint
run: | run: |

View file

@ -27,19 +27,24 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
BUNDLE_ONLY: development
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Ruby environment - name: Set up Ruby
uses: ./.github/actions/setup-ruby uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set-up RuboCop Problem Matcher - name: Set-up RuboCop Problem Matcher
uses: r7kamura/rubocop-problem-matchers-action@v1 uses: r7kamura/rubocop-problem-matchers-action@v1
- name: Run rubocop - name: Run rubocop
run: bundle exec rubocop run: bin/rubocop
- name: Run brakeman - name: Run brakeman
if: always() # Run both checks, even if the first failed if: always() # Run both checks, even if the first failed
run: bundle exec brakeman run: bin/brakeman

View file

@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Check for merge conflicts - name: Check for merge conflicts
uses: eps1lon/actions-label-merge-conflict@releases/2.x uses: eps1lon/actions-label-merge-conflict@v3
with: with:
dirtyLabel: 'rebase needed :construction:' dirtyLabel: 'rebase needed :construction:'
repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'

View file

@ -1,95 +0,0 @@
name: Test two step migrations
on:
push:
branches-ignore:
- 'dependabot/**'
- 'renovate/**'
pull_request:
jobs:
pre_job:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-two-step.yml", "lib/tasks/tests.rake"]'
test:
runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
strategy:
fail-fast: false
matrix:
postgres:
- 14-alpine
- 15-alpine
services:
postgres:
image: postgres:${{ matrix.postgres}}
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:
CONTINUOUS_INTEGRATION: true
DB_HOST: localhost
DB_USER: postgres
DB_PASS: postgres
DISABLE_SIMPLECOV: true
RAILS_ENV: test
BUNDLE_CLEAN: true
BUNDLE_FROZEN: true
BUNDLE_WITHOUT: 'development production'
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
steps:
- uses: actions/checkout@v4
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
- name: Create database
run: './bin/rails db:create'
- name: Run historical migrations with data population
run: './bin/rails tests:migrations:prepare_database'
env:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- name: Run all remaining pre-deployment migrations
run: './bin/rails db:migrate'
env:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- name: Run all post-deployment migrations
run: './bin/rails db:migrate'
- name: Check migration result
run: './bin/rails tests:migrations:check_database'

View file

@ -1,4 +1,5 @@
name: Test one step migrations name: Historical data migration test
on: on:
push: push:
branches-ignore: branches-ignore:
@ -17,7 +18,7 @@ jobs:
- id: skip_check - id: skip_check
uses: fkirc/skip-duplicate-actions@v5 uses: fkirc/skip-duplicate-actions@v5
with: with:
paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-one-step.yml", "lib/tasks/tests.rake"]' paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations.yml", "lib/tasks/tests.rake"]'
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -40,9 +41,9 @@ jobs:
POSTGRES_USER: postgres POSTGRES_USER: postgres
options: >- options: >-
--health-cmd pg_isready --health-cmd pg_isready
--health-interval 10s --health-interval 10ms
--health-timeout 5s --health-timeout 3s
--health-retries 5 --health-retries 50
ports: ports:
- 5432:5432 - 5432:5432
@ -50,14 +51,13 @@ jobs:
image: redis:7-alpine image: redis:7-alpine
options: >- options: >-
--health-cmd "redis-cli ping" --health-cmd "redis-cli ping"
--health-interval 10s --health-interval 10ms
--health-timeout 5s --health-timeout 3s
--health-retries 5 --health-retries 50
ports: ports:
- 6379:6379 - 6379:6379
env: env:
CONTINUOUS_INTEGRATION: true
DB_HOST: localhost DB_HOST: localhost
DB_USER: postgres DB_USER: postgres
DB_PASS: postgres DB_PASS: postgres
@ -65,7 +65,7 @@ jobs:
RAILS_ENV: test RAILS_ENV: test
BUNDLE_CLEAN: true BUNDLE_CLEAN: true
BUNDLE_FROZEN: true BUNDLE_FROZEN: true
BUNDLE_WITHOUT: 'development production' BUNDLE_WITHOUT: 'development:production'
BUNDLE_JOBS: 3 BUNDLE_JOBS: 3
BUNDLE_RETRY: 3 BUNDLE_RETRY: 3
@ -75,14 +75,19 @@ jobs:
- name: Set up Ruby environment - name: Set up Ruby environment
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
- name: Create database - name: Test "one step migration" flow
run: './bin/rails db:create' run: |
bin/rails db:drop
bin/rails db:create
bin/rails tests:migrations:prepare_database
bin/rails db:migrate
bin/rails tests:migrations:check_database
- name: Run historical migrations with data population - name: Test "two step migration" flow
run: './bin/rails tests:migrations:prepare_database' run: |
bin/rails db:drop
- name: Run all remaining migrations bin/rails db:create
run: './bin/rails db:migrate' SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate
- name: Check migration result bin/rails db:migrate
run: './bin/rails tests:migrations:check_database' bin/rails tests:migrations:check_database

View file

@ -28,11 +28,7 @@ jobs:
env: env:
RAILS_ENV: ${{ matrix.mode }} RAILS_ENV: ${{ matrix.mode }}
BUNDLE_WITH: ${{ matrix.mode }} BUNDLE_WITH: ${{ matrix.mode }}
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: precompile_placeholder SECRET_KEY_BASE_DUMMY: 1
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: precompile_placeholder
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: precompile_placeholder
OTP_SECRET: precompile_placeholder
SECRET_KEY_BASE: precompile_placeholder
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -77,9 +73,9 @@ jobs:
POSTGRES_USER: postgres POSTGRES_USER: postgres
options: >- options: >-
--health-cmd pg_isready --health-cmd pg_isready
--health-interval 10s --health-interval 10ms
--health-timeout 5s --health-timeout 3s
--health-retries 5 --health-retries 50
ports: ports:
- 5432:5432 - 5432:5432
@ -87,9 +83,9 @@ jobs:
image: redis:7-alpine image: redis:7-alpine
options: >- options: >-
--health-cmd "redis-cli ping" --health-cmd "redis-cli ping"
--health-interval 10s --health-interval 10ms
--health-timeout 5s --health-timeout 3s
--health-retries 5 --health-retries 50
ports: ports:
- 6379:6379 - 6379:6379
@ -133,7 +129,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg imagemagick libpam-dev additional-system-dependencies: ffmpeg libpam-dev
- name: Load database schema - name: Load database schema
run: './bin/rails db:create db:schema:load db:seed' run: './bin/rails db:create db:schema:load db:seed'
@ -148,6 +144,93 @@ jobs:
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-libvips:
name: Libvips tests
runs-on: ubuntu-24.04
needs:
- build
services:
postgres:
image: postgres:14-alpine
env:
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
options: >-
--health-cmd pg_isready
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 5432:5432
redis:
image: redis:7-alpine
options: >-
--health-cmd "redis-cli ping"
--health-interval 10ms
--health-timeout 3s
--health-retries 50
ports:
- 6379:6379
env:
DB_HOST: localhost
DB_USER: postgres
DB_PASS: postgres
DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }}
RAILS_ENV: test
ALLOW_NOPAM: true
PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled
OIDC_ENABLED: true
OIDC_SCOPE: read
SAML_ENABLED: true
CAS_ENABLED: true
BUNDLE_WITH: 'pam_authentication test'
GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }}
MASTODON_USE_LIBVIPS: true
strategy:
fail-fast: false
matrix:
ruby-version:
- '3.1'
- '3.2'
- '.ruby-version'
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: './'
name: ${{ github.sha }}
- name: Expand archived asset artifacts
run: |
tar xvzf artifacts.tar.gz
- name: Set up Ruby environment
uses: ./.github/actions/setup-ruby
with:
ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg libpam-dev libyaml-dev
- name: Load database schema
run: './bin/rails db:create db:schema:load db:seed'
- run: bin/rspec --tag paperclip_processing
- name: Upload coverage reports to Codecov
if: matrix.ruby-version == '.ruby-version'
uses: codecov/codecov-action@v4
with:
files: coverage/lcov/mastodon.lcov
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
test-e2e: test-e2e:
name: End to End testing name: End to End testing
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -163,9 +246,9 @@ jobs:
POSTGRES_USER: postgres POSTGRES_USER: postgres
options: >- options: >-
--health-cmd pg_isready --health-cmd pg_isready
--health-interval 10s --health-interval 10ms
--health-timeout 5s --health-timeout 3s
--health-retries 5 --health-retries 50
ports: ports:
- 5432:5432 - 5432:5432
@ -173,9 +256,9 @@ jobs:
image: redis:7-alpine image: redis:7-alpine
options: >- options: >-
--health-cmd "redis-cli ping" --health-cmd "redis-cli ping"
--health-interval 10s --health-interval 10ms
--health-timeout 5s --health-timeout 3s
--health-retries 5 --health-retries 50
ports: ports:
- 6379:6379 - 6379:6379
@ -209,7 +292,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg imagemagick additional-system-dependencies: ffmpeg
- name: Set up Javascript environment - name: Set up Javascript environment
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript
@ -248,9 +331,9 @@ jobs:
POSTGRES_USER: postgres POSTGRES_USER: postgres
options: >- options: >-
--health-cmd pg_isready --health-cmd pg_isready
--health-interval 10s --health-interval 10ms
--health-timeout 5s --health-timeout 3s
--health-retries 5 --health-retries 50
ports: ports:
- 5432:5432 - 5432:5432
@ -258,9 +341,9 @@ jobs:
image: redis:7-alpine image: redis:7-alpine
options: >- options: >-
--health-cmd "redis-cli ping" --health-cmd "redis-cli ping"
--health-interval 10s --health-interval 10ms
--health-timeout 5s --health-timeout 3s
--health-retries 5 --health-retries 50
ports: ports:
- 6379:6379 - 6379:6379
@ -271,9 +354,9 @@ jobs:
xpack.security.enabled: false xpack.security.enabled: false
options: >- options: >-
--health-cmd "curl http://localhost:9200/_cluster/health" --health-cmd "curl http://localhost:9200/_cluster/health"
--health-interval 10s --health-interval 2s
--health-timeout 5s --health-timeout 3s
--health-retries 10 --health-retries 50
ports: ports:
- 9200:9200 - 9200:9200
@ -285,9 +368,9 @@ jobs:
DISABLE_SECURITY_PLUGIN: true DISABLE_SECURITY_PLUGIN: true
options: >- options: >-
--health-cmd "curl http://localhost:9200/_cluster/health" --health-cmd "curl http://localhost:9200/_cluster/health"
--health-interval 10s --health-interval 2s
--health-timeout 5s --health-timeout 3s
--health-retries 10 --health-retries 50
ports: ports:
- 9200:9200 - 9200:9200
@ -329,7 +412,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg imagemagick additional-system-dependencies: ffmpeg
- name: Set up Javascript environment - name: Set up Javascript environment
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript

View file

@ -1,19 +0,0 @@
.DS_Store
.git/
.gitignore
.bundle/
.cache/
config/deploy/*
coverage
docs/
.env
log/*.log
neo4j/
node_modules/
public/assets/
public/system/
spec/
tmp/
.vagrant/
vendor/bundle/

View file

@ -1,7 +1,34 @@
# Can be removed once all rules are addressed or moved to this file as documented overrides ---
inherit_from: .rubocop_todo.yml AllCops:
CacheRootDirectory: tmp
DisplayCopNames: true
DisplayStyleGuide: true
Exclude:
- db/schema.rb
- bin/*
- node_modules/**/*
- Vagrantfile
- vendor/**/*
- config/initializers/json_ld*
- lib/mastodon/migration_helpers.rb
- lib/templates/**/*
ExtraDetails: true
NewCops: enable
TargetRubyVersion: 3.1 # Oldest supported ruby version
UseCache: true
inherit_from:
- .rubocop/layout.yml
- .rubocop/metrics.yml
- .rubocop/naming.yml
- .rubocop/rails.yml
- .rubocop/rspec_rails.yml
- .rubocop/rspec.yml
- .rubocop/style.yml
- .rubocop/custom.yml
- .rubocop_todo.yml
- .rubocop/strict.yml
# Used for merging with exclude lists with .rubocop_todo.yml
inherit_mode: inherit_mode:
merge: merge:
- Exclude - Exclude
@ -12,229 +39,3 @@ require:
- rubocop-rspec_rails - rubocop-rspec_rails
- rubocop-performance - rubocop-performance
- rubocop-capybara - rubocop-capybara
- ./lib/linter/rubocop_middle_dot
AllCops:
TargetRubyVersion: 3.1 # Set to minimum supported version of CI
DisplayCopNames: true
DisplayStyleGuide: true
ExtraDetails: true
UseCache: true
CacheRootDirectory: tmp
NewCops: enable # Opt-in to newly added rules
Exclude:
- db/schema.rb
- 'bin/*'
- 'node_modules/**/*'
- 'Vagrantfile'
- 'vendor/**/*'
- 'config/initializers/json_ld*' # Generated files
- 'lib/mastodon/migration_helpers.rb' # Vendored from GitLab
- 'lib/templates/**/*'
# Reason: Prefer Hashes without extreme indentation
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutfirsthashelementindentation
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
# Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
Layout/LineLength:
Max: 300 # Default of 120 causes a duplicate entry in generated todo file
## Disable most Metrics/*Length cops
# Reason: those are often triggered and force significant refactors when this happend
# but the team feel they are not really improving the code quality.
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength
Metrics/BlockLength:
Enabled: false
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength
Metrics/ClassLength:
Enabled: false
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength
Metrics/MethodLength:
Enabled: false
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
Metrics/ModuleLength:
Enabled: false
## End Disable Metrics/*Length cops
# Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize
Metrics/AbcSize:
Exclude:
- 'lib/mastodon/cli/*.rb'
# Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
Metrics/CyclomaticComplexity:
Exclude:
- lib/mastodon/cli/*.rb
# Reason:
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists
Metrics/ParameterLists:
CountKeywordArgs: false
# Reason: Prefer seeing a variable name
# https://docs.rubocop.org/rubocop/cops_naming.html#namingblockforwarding
Naming/BlockForwarding:
EnforcedStyle: explicit
# Reason: Prevailing style is argument file paths
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
Rails/FilePath:
EnforcedStyle: arguments
# Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus
Rails/HttpStatus:
EnforcedStyle: numeric
# Reason: Conflicts with `Lint/UselessMethodDefinition` for inherited controller actions
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railslexicallyscopedactionfilter
Rails/LexicallyScopedActionFilter:
Exclude:
- 'app/controllers/auth/*'
# Reason: These tasks are doing local work which do not need full env loaded
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsrakeenvironment
Rails/RakeEnvironment:
Exclude:
- 'lib/tasks/auto_annotate_models.rake'
- 'lib/tasks/emojis.rake'
- 'lib/tasks/mastodon.rake'
- 'lib/tasks/repo.rake'
- 'lib/tasks/statistics.rake'
# Reason: There are appropriate times to use these features
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsskipsmodelvalidations
Rails/SkipsModelValidations:
Enabled: false
# Reason: We want to preserve the ability to migrate from arbitrary old versions,
# and cannot guarantee that every installation has run every migration as they upgrade.
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsunusedignoredcolumns
Rails/UnusedIgnoredColumns:
Enabled: false
# Reason: Prevailing style choice
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsnegateinclude
Rails/NegateInclude:
Enabled: false
# Reason: Enforce default limit, but allow some elements to span lines
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecexamplelength
RSpec/ExampleLength:
CountAsOne: ['array', 'heredoc', 'method_call']
# Reason: Deprecated cop, will be removed in 3.0, replaced by SpecFilePathFormat
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath
RSpec/FilePath:
Enabled: false
# Reason:
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject
RSpec/NamedSubject:
EnforcedStyle: named_only
# Reason: Prevailing style choice
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnottonot
RSpec/NotToNot:
EnforcedStyle: to_not
# Reason: Match overrides from Rspec/FilePath rule above
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat
RSpec/SpecFilePathFormat:
CustomTransform:
ActivityPub: activitypub
DeepL: deepl
FetchOEmbedService: fetch_oembed_service
OEmbedController: oembed_controller
OStatus: ostatus
# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus
# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus
RSpecRails/HttpStatus:
EnforcedStyle: numeric
# Reason:
# 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
# Reason: Route redirects are not token-formatted and must be skipped
# https://docs.rubocop.org/rubocop/cops_style.html#styleformatstringtoken
Style/FormatStringToken:
inherit_mode:
merge:
- AllowedMethods # The rubocop-rails config adds `redirect`
AllowedMethods:
- redirect_with_vary
# Reason: Prevailing style choice
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashaslastarrayitem
Style/HashAsLastArrayItem:
Enabled: false
# Reason: Enforce modern Ruby style
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax
Style/HashSyntax:
EnforcedStyle: ruby19_no_mixed_keys
EnforcedShorthandSyntax: either
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals
Style/NumericLiterals:
AllowedPatterns:
- \d{4}_\d{2}_\d{2}_\d{6} # For DB migration date version number readability
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#stylepercentliteraldelimiters
Style/PercentLiteralDelimiters:
PreferredDelimiters:
'%i': '()'
'%w': '()'
# Reason: Prefer less indentation in conditional assignments
# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantbegin
Style/RedundantBegin:
Enabled: false
# Reason: Prevailing style choice
# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantfetchblock
Style/RedundantFetchBlock:
Enabled: false
# Reason: Overridden to reduce implicit StandardError rescues
# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror
Style/RescueStandardError:
EnforcedStyle: implicit
# Reason: Originally disabled for CodeClimate, and no config consensus has been found
# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray
Style/SymbolArray:
Enabled: false
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainarrayliteral
Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: 'comma'
# Reason:
# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma'
Style/MiddleDot:
Enabled: true

6
.rubocop/custom.yml Normal file
View file

@ -0,0 +1,6 @@
---
require:
- ../lib/linter/rubocop_middle_dot
Style/MiddleDot:
Enabled: true

6
.rubocop/layout.yml Normal file
View file

@ -0,0 +1,6 @@
---
Layout/FirstHashElementIndentation:
EnforcedStyle: consistent
Layout/LineLength:
Max: 300 # Default of 120 causes a duplicate entry in generated todo file

23
.rubocop/metrics.yml Normal file
View file

@ -0,0 +1,23 @@
---
Metrics/AbcSize:
Exclude:
- lib/mastodon/cli/*.rb
Metrics/BlockLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Exclude:
- lib/mastodon/cli/*.rb
Metrics/MethodLength:
Enabled: false
Metrics/ModuleLength:
Enabled: false
Metrics/ParameterLists:
CountKeywordArgs: false

3
.rubocop/naming.yml Normal file
View file

@ -0,0 +1,3 @@
---
Naming/BlockForwarding:
EnforcedStyle: explicit

27
.rubocop/rails.yml Normal file
View file

@ -0,0 +1,27 @@
---
Rails/FilePath:
EnforcedStyle: arguments
Rails/HttpStatus:
EnforcedStyle: numeric
Rails/LexicallyScopedActionFilter:
Exclude:
- app/controllers/auth/* # Conflicts with `Lint/UselessMethodDefinition` for inherited controller actions
Rails/NegateInclude:
Enabled: false
Rails/RakeEnvironment:
Exclude: # Tasks are doing local work which do not need full env loaded
- lib/tasks/auto_annotate_models.rake
- lib/tasks/emojis.rake
- lib/tasks/mastodon.rake
- lib/tasks/repo.rake
- lib/tasks/statistics.rake
Rails/SkipsModelValidations:
Enabled: false
Rails/UnusedIgnoredColumns:
Enabled: false # Preserve ability to migrate from arbitrary old versions

27
.rubocop/rspec.yml Normal file
View file

@ -0,0 +1,27 @@
---
RSpec/ExampleLength:
CountAsOne: ['array', 'heredoc', 'method_call']
Max: 20 # Override default of 5
RSpec/MultipleExpectations:
Max: 10 # Overrides default of 1
RSpec/MultipleMemoizedHelpers:
Max: 20 # Overrides default of 5
RSpec/NamedSubject:
EnforcedStyle: named_only
RSpec/NestedGroups:
Max: 10 # Overrides default of 3
RSpec/NotToNot:
EnforcedStyle: to_not
RSpec/SpecFilePathFormat:
CustomTransform:
ActivityPub: activitypub
DeepL: deepl
FetchOEmbedService: fetch_oembed_service
OEmbedController: oembed_controller
OStatus: ostatus

3
.rubocop/rspec_rails.yml Normal file
View file

@ -0,0 +1,3 @@
---
RSpecRails/HttpStatus:
EnforcedStyle: numeric

19
.rubocop/strict.yml Normal file
View file

@ -0,0 +1,19 @@
Lint/Debugger: # Remove any `binding.pry`
Enabled: true
Exclude: []
RSpec/Focus: # Require full spec run on CI
Enabled: true
Exclude: []
Rails/Output: # Remove any `puts` debugging
Enabled: true
Exclude: []
Rails/FindEach: # Using `each` could impact performance, use `find_each`
Enabled: true
Exclude: []
Rails/UniqBeforePluck: # Require `uniq.pluck` and not `pluck.uniq`
Enabled: true
Exclude: []

47
.rubocop/style.yml Normal file
View file

@ -0,0 +1,47 @@
---
Style/ClassAndModuleChildren:
Enabled: false
Style/Documentation:
Enabled: false
Style/FormatStringToken:
AllowedMethods:
- redirect_with_vary # Route redirects are not token-formatted
inherit_mode:
merge:
- AllowedMethods
Style/HashAsLastArrayItem:
Enabled: false
Style/HashSyntax:
EnforcedShorthandSyntax: either
EnforcedStyle: ruby19_no_mixed_keys
Style/NumericLiterals:
AllowedPatterns:
- \d{4}_\d{2}_\d{2}_\d{6}
Style/PercentLiteralDelimiters:
PreferredDelimiters:
'%i': ()
'%w': ()
Style/RedundantBegin:
Enabled: false
Style/RedundantFetchBlock:
Enabled: false
Style/RescueStandardError:
EnforcedStyle: implicit
Style/SymbolArray:
Enabled: false
Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: comma
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: comma

View file

@ -27,21 +27,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 27 Max: 27
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 18
RSpec/MultipleExpectations:
Max: 7
# Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers:
Max: 17
# Configuration parameters: AllowedGroups.
RSpec/NestedGroups:
Max: 6
Rails/OutputSafety: Rails/OutputSafety:
Exclude: Exclude:
- 'config/initializers/simple_form.rb' - 'config/initializers/simple_form.rb'

View file

@ -1 +1 @@
3.3.2 3.3.3

View file

@ -1,5 +1,8 @@
# syntax=docker/dockerfile:1.7 # syntax=docker/dockerfile:1.7
# This file is designed for production server deployment, not local development work
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker
# Please see https://docs.docker.com/engine/reference/builder for information about # Please see https://docs.docker.com/engine/reference/builder for information about
# the extended buildx capabilities used in this file. # the extended buildx capabilities used in this file.
# Make sure multiarch TARGETPLATFORM is available for interpolation # Make sure multiarch TARGETPLATFORM is available for interpolation
@ -7,22 +10,24 @@
ARG TARGETPLATFORM=${TARGETPLATFORM} ARG TARGETPLATFORM=${TARGETPLATFORM}
ARG BUILDPLATFORM=${BUILDPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM}
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.1"] # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
ARG RUBY_VERSION="3.3.1" # renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.3.3"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="20" ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm" ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) # Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
# Ruby image to use for base image based on combined variables (ex: 3.3.1-slim-bookworm) # Ruby image to use for base image based on combined variables (ex: 3.3.x-slim-bookworm)
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
# Example: v4.2.0-nightly.2023.11.09+something # Example: v4.2.0-nightly.2023.11.09+something
# Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] # Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
ARG MASTODON_VERSION_PRERELEASE="bark" ARG MASTODON_VERSION_PRERELEASE="bark"
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"] # Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-12345"]
ARG MASTODON_VERSION_METADATA="dev" ARG MASTODON_VERSION_METADATA="dev"
# Allow Ruby on Rails to serve static files # Allow Ruby on Rails to serve static files
@ -60,7 +65,9 @@ ENV \
DEBIAN_FRONTEND="noninteractive" \ DEBIAN_FRONTEND="noninteractive" \
PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \
# Optimize jemalloc 5.x performance # Optimize jemalloc 5.x performance
MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \
# Enable libvips, should not be changed
MASTODON_USE_LIBVIPS=true
# Set default shell used for running commands # Set default shell used for running commands
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
@ -97,7 +104,6 @@ RUN \
curl \ curl \
ffmpeg \ ffmpeg \
file \ file \
imagemagick \
libjemalloc2 \ libjemalloc2 \
patchelf \ patchelf \
procps \ procps \
@ -131,18 +137,31 @@ RUN \
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Install build tools and bundler dependencies from APT # Install build tools and bundler dependencies from APT
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
g++ \ build-essential \
gcc \
git \ git \
libgdbm-dev \ libgdbm-dev \
libglib2.0-dev \
libgmp-dev \ libgmp-dev \
libicu-dev \ libicu-dev \
libidn-dev \ libidn-dev \
libpq-dev \ libpq-dev \
libssl-dev \ libssl-dev \
make \ meson \
pkg-config \
shared-mime-info \ shared-mime-info \
zlib1g-dev \ # libvips components
libcgif-dev \
libexif-dev \
libexpat1-dev \
libgirepository1.0-dev \
libheif-dev \
libimagequant-dev \
libjpeg62-turbo-dev \
liblcms2-dev \
liborc-dev \
libspng-dev \
libtiff-dev \
libwebp-dev \
; ;
RUN \ RUN \
@ -151,6 +170,26 @@ RUN \
corepack enable; \ corepack enable; \
corepack prepare --activate; corepack prepare --activate;
# Create temporary libvips specific build layer from build layer
FROM build as libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.15.2
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
WORKDIR /usr/local/libvips/src
RUN \
curl -sSL -o vips-${VIPS_VERSION}.tar.xz ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz; \
tar xf vips-${VIPS_VERSION}.tar.xz; \
cd vips-${VIPS_VERSION}; \
meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \
cd build; \
ninja; \
ninja install;
# Create temporary bundler specific build layer from build layer # Create temporary bundler specific build layer from build layer
FROM build as bundler FROM build as bundler
@ -200,16 +239,16 @@ COPY . /opt/mastodon/
COPY --from=yarn /opt/mastodon /opt/mastodon/ COPY --from=yarn /opt/mastodon /opt/mastodon/
COPY --from=bundler /opt/mastodon /opt/mastodon/ COPY --from=bundler /opt/mastodon /opt/mastodon/
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
# Copy libvips components to layer for precompiler
COPY --from=libvips /usr/local/libvips/bin /usr/local/bin
COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN \ RUN \
ldconfig; \
# Use Ruby on Rails to create Mastodon assets # Use Ruby on Rails to create Mastodon assets
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=precompile_placeholder \ SECRET_KEY_BASE_DUMMY=1 \
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=precompile_placeholder \
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=precompile_placeholder \
OTP_SECRET=precompile_placeholder \
SECRET_KEY_BASE=precompile_placeholder \
bundle exec rails assets:precompile; \ bundle exec rails assets:precompile; \
# Cleanup temporary files # Cleanup temporary files
rm -fr /opt/mastodon/tmp; rm -fr /opt/mastodon/tmp;
@ -229,12 +268,27 @@ RUN \
--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \
# Apt update install non-dev versions of necessary components # Apt update install non-dev versions of necessary components
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
libssl3 \ libexpat1 \
libpq5 \ libglib2.0-0 \
libicu72 \ libicu72 \
libidn12 \ libidn12 \
libpq5 \
libreadline8 \ libreadline8 \
libssl3 \
libyaml-0-2 \ libyaml-0-2 \
# libvips components
libcgif0 \
libexif12 \
libheif1 \
libimagequant0 \
libjpeg62-turbo \
liblcms2-2 \
liborc-0.4-0 \
libspng0 \
libtiff6 \
libwebp7 \
libwebpdemux2 \
libwebpmux3 \
; ;
# Copy Mastodon sources into final layer # Copy Mastodon sources into final layer
@ -245,9 +299,17 @@ COPY --from=precompiler /opt/mastodon/public/packs /opt/mastodon/public/packs
COPY --from=precompiler /opt/mastodon/public/assets /opt/mastodon/public/assets COPY --from=precompiler /opt/mastodon/public/assets /opt/mastodon/public/assets
# Copy bundler components to layer # Copy bundler components to layer
COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
# Copy libvips components to layer
COPY --from=libvips /usr/local/libvips/bin /usr/local/bin
COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
RUN \ RUN \
# Precompile bootsnap code for faster Rails startup ldconfig; \
# Smoketest media processors
vips -v;
RUN \
# Precompile bootsnap code for faster Rails startup
bundle exec bootsnap precompile --gemfile app/ lib/; bundle exec bootsnap precompile --gemfile app/ lib/;
RUN \ RUN \

View file

@ -23,6 +23,7 @@ gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 1.0', require: false gem 'fog-openstack', '~> 1.0', require: false
gem 'kt-paperclip', '~> 7.2' gem 'kt-paperclip', '~> 7.2'
gem 'md-paperclip-azure', '~> 2.2', require: false gem 'md-paperclip-azure', '~> 2.2', require: false
gem 'ruby-vips', '~> 2.2', require: false
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8' gem 'addressable', '~> 2.8'
@ -56,7 +57,7 @@ gem 'hiredis', '~> 0.6'
gem 'htmlentities', '~> 4.3' gem 'htmlentities', '~> 4.3'
gem 'http', '~> 5.2.0' gem 'http', '~> 5.2.0'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 1.6.2' gem 'httplog', '~> 1.7.0'
gem 'i18n' gem 'i18n'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'inline_svg' gem 'inline_svg'
@ -106,7 +107,7 @@ gem 'private_address_check', '~> 0.5'
gem 'opentelemetry-api', '~> 1.2.5' gem 'opentelemetry-api', '~> 1.2.5'
group :opentelemetry do group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.26.3', require: false gem 'opentelemetry-exporter-otlp', '~> 0.27.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false
@ -170,6 +171,7 @@ group :development do
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
gem 'rubocop-rspec_rails', require: false
# Annotates modules with schema # Annotates modules with schema
gem 'annotate', '~> 3.2' gem 'annotate', '~> 3.2'

View file

@ -10,35 +10,35 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.1.3.3) actioncable (7.1.3.4)
actionpack (= 7.1.3.3) actionpack (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.1.3.3) actionmailbox (7.1.3.4)
actionpack (= 7.1.3.3) actionpack (= 7.1.3.4)
activejob (= 7.1.3.3) activejob (= 7.1.3.4)
activerecord (= 7.1.3.3) activerecord (= 7.1.3.4)
activestorage (= 7.1.3.3) activestorage (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.3.3) actionmailer (7.1.3.4)
actionpack (= 7.1.3.3) actionpack (= 7.1.3.4)
actionview (= 7.1.3.3) actionview (= 7.1.3.4)
activejob (= 7.1.3.3) activejob (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.1.3.3) actionpack (7.1.3.4)
actionview (= 7.1.3.3) actionview (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4) rack (>= 2.2.4)
@ -46,15 +46,15 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
actiontext (7.1.3.3) actiontext (7.1.3.4)
actionpack (= 7.1.3.3) actionpack (= 7.1.3.4)
activerecord (= 7.1.3.3) activerecord (= 7.1.3.4)
activestorage (= 7.1.3.3) activestorage (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.3.3) actionview (7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
@ -64,22 +64,22 @@ GEM
activemodel (>= 4.1) activemodel (>= 4.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.1.3.3) activejob (7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.3.3) activemodel (7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
activerecord (7.1.3.3) activerecord (7.1.3.4)
activemodel (= 7.1.3.3) activemodel (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.3.3) activestorage (7.1.3.4)
actionpack (= 7.1.3.3) actionpack (= 7.1.3.4)
activejob (= 7.1.3.3) activejob (= 7.1.3.4)
activerecord (= 7.1.3.3) activerecord (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.3.3) activesupport (7.1.3.4)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@ -100,17 +100,17 @@ GEM
attr_required (1.0.2) attr_required (1.0.2)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.929.0) aws-partitions (1.940.0)
aws-sdk-core (3.196.1) aws-sdk-core (3.197.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.81.0) aws-sdk-kms (1.83.0)
aws-sdk-core (~> 3, >= 3.193.0) aws-sdk-core (~> 3, >= 3.197.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.151.0) aws-sdk-s3 (1.152.3)
aws-sdk-core (~> 3, >= 3.194.0) aws-sdk-core (~> 3, >= 3.197.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0) aws-sigv4 (1.8.0)
@ -168,7 +168,7 @@ GEM
climate_control (1.2.0) climate_control (1.2.0)
cocoon (1.2.15) cocoon (1.2.15)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.3.1) concurrent-ruby (1.3.3)
connection_pool (2.4.1) connection_pool (2.4.1)
cose (1.3.0) cose (1.3.0)
cbor (~> 0.5.9) cbor (~> 0.5.9)
@ -272,7 +272,7 @@ GEM
fog-json (1.2.0) fog-json (1.2.0)
fog-core fog-core
multi_json (~> 1.10) multi_json (~> 1.10)
fog-openstack (1.1.1) fog-openstack (1.1.3)
fog-core (~> 2.1) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
formatador (1.1.0) formatador (1.1.0)
@ -321,7 +321,7 @@ GEM
http-form_data (2.3.0) http-form_data (2.3.0)
http_accept_language (2.1.1) http_accept_language (2.1.1)
httpclient (2.8.3) httpclient (2.8.3)
httplog (1.6.3) httplog (1.7.0)
rack (>= 2.0) rack (>= 2.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.14.5) i18n (1.14.5)
@ -422,9 +422,9 @@ GEM
memory_profiler (1.0.1) memory_profiler (1.0.1)
mime-types (3.5.2) mime-types (3.5.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2024.0507) mime-types-data (3.2024.0604)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.6) mini_portile2 (2.8.7)
minitest (5.23.1) minitest (5.23.1)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
@ -434,7 +434,7 @@ GEM
uri uri
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.11) net-imap (0.4.12)
date date
net-protocol net-protocol
net-ldap (0.19.0) net-ldap (0.19.0)
@ -445,7 +445,7 @@ GEM
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.3) nio4r (2.7.3)
nokogiri (1.16.5) nokogiri (1.16.6)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nsa (0.3.0) nsa (0.3.0)
@ -453,7 +453,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5) sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.3) oj (3.16.4)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
omniauth (2.1.2) omniauth (2.1.2)
hashie (>= 3.4.6) hashie (>= 3.4.6)
@ -489,7 +489,7 @@ GEM
opentelemetry-api (1.2.5) opentelemetry-api (1.2.5)
opentelemetry-common (0.20.1) opentelemetry-common (0.20.1)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.26.3) opentelemetry-exporter-otlp (0.27.0)
google-protobuf (~> 3.14) google-protobuf (~> 3.14)
googleapis-common-protos-types (~> 1.3) googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1) opentelemetry-api (~> 1.1)
@ -498,6 +498,10 @@ GEM
opentelemetry-semantic_conventions opentelemetry-semantic_conventions
opentelemetry-helpers-sql-obfuscation (0.1.0) opentelemetry-helpers-sql-obfuscation (0.1.0)
opentelemetry-common (~> 0.20) opentelemetry-common (~> 0.20)
opentelemetry-instrumentation-action_mailer (0.1.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-action_pack (0.9.0) opentelemetry-instrumentation-action_pack (0.9.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
@ -551,8 +555,9 @@ GEM
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0) opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rails (0.30.1) opentelemetry-instrumentation-rails (0.30.2)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
opentelemetry-instrumentation-action_pack (~> 0.9.0) opentelemetry-instrumentation-action_pack (~> 0.9.0)
opentelemetry-instrumentation-action_view (~> 0.7.0) opentelemetry-instrumentation-action_view (~> 0.7.0)
opentelemetry-instrumentation-active_job (~> 0.7.0) opentelemetry-instrumentation-active_job (~> 0.7.0)
@ -578,15 +583,15 @@ GEM
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.14.18) ox (2.14.18)
parallel (1.24.0) parallel (1.25.1)
parser (3.3.2.0) parser (3.3.3.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
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.5.6) pg (1.5.6)
pghero (3.4.1) pghero (3.5.0)
activerecord (>= 6) activerecord (>= 6)
premailer (1.23.0) premailer (1.23.0)
addressable addressable
@ -634,20 +639,20 @@ GEM
rackup (1.0.0) rackup (1.0.0)
rack (< 3) rack (< 3)
webrick webrick
rails (7.1.3.3) rails (7.1.3.4)
actioncable (= 7.1.3.3) actioncable (= 7.1.3.4)
actionmailbox (= 7.1.3.3) actionmailbox (= 7.1.3.4)
actionmailer (= 7.1.3.3) actionmailer (= 7.1.3.4)
actionpack (= 7.1.3.3) actionpack (= 7.1.3.4)
actiontext (= 7.1.3.3) actiontext (= 7.1.3.4)
actionview (= 7.1.3.3) actionview (= 7.1.3.4)
activejob (= 7.1.3.3) activejob (= 7.1.3.4)
activemodel (= 7.1.3.3) activemodel (= 7.1.3.4)
activerecord (= 7.1.3.3) activerecord (= 7.1.3.4)
activestorage (= 7.1.3.3) activestorage (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.3.3) railties (= 7.1.3.4)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
@ -662,9 +667,9 @@ GEM
rails-i18n (7.0.9) rails-i18n (7.0.9)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
railties (7.1.3.3) railties (7.1.3.4)
actionpack (= 7.1.3.3) actionpack (= 7.1.3.4)
activesupport (= 7.1.3.3) activesupport (= 7.1.3.4)
irb irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -686,15 +691,15 @@ GEM
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.9.2) regexp_parser (2.9.2)
reline (0.5.7) reline (0.5.8)
io-console (~> 0.5) io-console (~> 0.5)
request_store (1.6.0) request_store (1.6.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.2.8) rexml (3.3.0)
strscan (>= 3.0.9) strscan
rotp (6.3.0) rotp (6.3.0)
rouge (4.2.1) rouge (4.2.1)
rpam2 (4.0.2) rpam2 (4.0.2)
@ -739,9 +744,7 @@ GEM
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3) rubocop-ast (1.31.3)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-capybara (2.20.0) rubocop-capybara (2.21.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.25.1)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-performance (1.21.0) rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
@ -751,25 +754,25 @@ GEM
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (2.29.2) rubocop-rspec (3.0.1)
rubocop (~> 1.40) rubocop (~> 1.61)
rubocop-capybara (~> 2.17) rubocop-rspec_rails (2.30.0)
rubocop-factory_bot (~> 2.22) rubocop (~> 1.61)
rubocop-rspec_rails (~> 2.28) rubocop-rspec (~> 3, >= 3.0.1)
rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40)
ruby-prof (1.7.0) ruby-prof (1.7.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.16.0) ruby-saml (1.16.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
rexml rexml
ruby-vips (2.2.1)
ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
rufus-scheduler (3.9.1) rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0) safety_net_attestation (0.4.0)
jwt (~> 2.0) jwt (~> 2.0)
sanitize (6.1.0) sanitize (6.1.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
scenic (1.8.0) scenic (1.8.0)
@ -895,7 +898,7 @@ GEM
xorcist (1.1.3) xorcist (1.1.3)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.14) zeitwerk (2.6.15)
PLATFORMS PLATFORMS
ruby ruby
@ -945,7 +948,7 @@ DEPENDENCIES
htmlentities (~> 4.3) htmlentities (~> 4.3)
http (~> 5.2.0) http (~> 5.2.0)
http_accept_language (~> 2.1) http_accept_language (~> 2.1)
httplog (~> 1.6.2) httplog (~> 1.7.0)
i18n i18n
i18n-tasks (~> 1.0) i18n-tasks (~> 1.0)
idn-ruby idn-ruby
@ -976,7 +979,7 @@ DEPENDENCIES
omniauth-saml (~> 2.0) omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1) omniauth_openid_connect (~> 0.6.1)
opentelemetry-api (~> 1.2.5) opentelemetry-api (~> 1.2.5)
opentelemetry-exporter-otlp (~> 0.26.3) opentelemetry-exporter-otlp (~> 0.27.0)
opentelemetry-instrumentation-active_job (~> 0.7.1) opentelemetry-instrumentation-active_job (~> 0.7.1)
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)
opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2)
@ -1021,8 +1024,10 @@ DEPENDENCIES
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
rubocop-rspec_rails
ruby-prof ruby-prof
ruby-progressbar (~> 1.13) ruby-progressbar (~> 1.13)
ruby-vips (~> 2.2)
rubyzip (~> 2.3) rubyzip (~> 2.3)
sanitize (~> 6.0) sanitize (~> 6.0)
scenic (~> 1.7) scenic (~> 1.7)
@ -1050,7 +1055,7 @@ DEPENDENCIES
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION RUBY VERSION
ruby 3.3.1p55 ruby 3.3.2p78
BUNDLED WITH BUNDLED WITH
2.5.9 2.5.11

View file

@ -62,7 +62,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
### Tech stack ### Tech stack
- **Ruby on Rails** powers the REST API and other web pages - **Ruby on Rails** powers the REST API and other web pages
- **React.js** and Redux are used for the dynamic parts of the interface - **React.js** and **Redux** are used for the dynamic parts of the interface
- **Node.js** powers the streaming API - **Node.js** powers the streaming API
### Requirements ### Requirements
@ -72,7 +72,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
- **Ruby** 3.1+ - **Ruby** 3.1+
- **Node.js** 18+ - **Node.js** 18+
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
## Development ## Development
@ -86,40 +86,51 @@ A **Vagrant** configuration is included for development purposes. To use it, com
- Run `vagrant ssh -c "cd /vagrant && bin/dev"` - Run `vagrant ssh -c "cd /vagrant && bin/dev"`
- Open `http://mastodon.local` in your browser - Open `http://mastodon.local` in your browser
### MacOS ### macOS
To set up **MacOS** for native development, complete the following steps: To set up **macOS** for native development, complete the following steps:
- Use a Ruby version manager to install the specified version from `.ruby-version` - Install [Homebrew] and run `brew install postgresql@14 redis imagemagick
- Run `bundle` to install required gems libidn nvm` to install the required project dependencies
- Run `brew install postgresql@14 redis imagemagick libidn` to install required dependencies - Use a Ruby version manager to activate the ruby in `.ruby-version` and run
- Navigate to Mastodon's root directory and run `brew install nvm` then `nvm use` to use the version from `.nvmrc` `nvm use` to activate the node version from `.nvmrc`
- Run `yarn` to install required packages - Run the `bin/setup` script, which will install the required ruby gems and node
- Run `corepack enable && corepack prepare` packages and prepare the database for local development
- Run `RAILS_ENV=development bundle exec rails db:setup` - Finally, run the `bin/dev` script which will launch services via `overmind`
- Finally, run `bin/dev` which will launch the local services via `overmind` (if installed) or `foreman` (if installed) or `foreman`
### Docker ### Docker
For development with **Docker**, complete the following steps: For production hosting and deployment with **Docker**, use the `Dockerfile` and
`docker-compose.yml` in the project root directory.
- Install Docker Desktop For local development, install and launch [Docker], and run:
- Run `docker compose -f .devcontainer/docker-compose.yml up -d`
- Run `docker compose -f .devcontainer/docker-compose.yml exec app .devcontainer/post-create.sh`
- Finally, run `docker compose -f .devcontainer/docker-compose.yml exec app bin/dev`
If you are using an IDE with [support for the Development Container specification](https://containers.dev/supporting), it will run the above `docker compose` commands automatically. For **Visual Studio Code** this requires the [Dev Container extension](https://containers.dev/supporting#dev-containers). ```shell
docker compose -f .devcontainer/compose.yaml up -d
docker compose -f .devcontainer/compose.yaml exec app bin/setup
docker compose -f .devcontainer/compose.yaml exec app bin/dev
```
### Dev Containers
Within IDEs that support the [Development Containers] specification, start the
"Mastodon on local machine" container from the editor. The necessary `docker
compose` commands to build and setup the container should run automatically. For
**Visual Studio Code** this requires installing the [Dev Container extension].
### GitHub Codespaces ### GitHub Codespaces
To get you coding in just a few minutes, GitHub Codespaces provides a web-based version of Visual Studio Code and a cloud-hosted development environment fully configured with the software needed for this project.. [GitHub Codespaces] provides a web-based version of VS Code and a cloud hosted
development environment configured with the software needed for this project.
- Click this button to create a new codespace:<br> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)][codespace]
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json)
- Wait for the environment to build. This will take a few minutes. - Click the button to create a new codespace, and confirm the options
- When the editor is ready, run `bin/dev` in the terminal. - Wait for the environment to build (takes a few minutes)
- After a few seconds, a popup will appear with a button labeled _Open in Browser_. This will open Mastodon. - When the editor is ready, run `bin/dev` in the terminal
- On the _Ports_ tab, right click on the “stream” row and select _Port visibility__Public_. - Wait for an _Open in Browser_ prompt. This will open Mastodon
- On the _Ports_ tab "stream" setting change _Port visibility_ → _Public_
## Contributing ## Contributing
@ -138,3 +149,10 @@ This program is free software: you can redistribute it and/or modify it under th
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
[codespace]: https://codespaces.new/mastodon/mastodon?quickstart=1&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json
[Dev Container extension]: https://containers.dev/supporting#dev-containers
[Development Containers]: https://containers.dev/supporting
[Docker]: https://docs.docker.com
[GitHub Codespaces]: https://docs.github.com/en/codespaces
[Homebrew]: https://brew.sh

View file

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

6
Vagrantfile vendored
View file

@ -151,6 +151,12 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
vb.customize ["modifyvm", :id, "--nictype2", "virtio"] vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
end end
config.vm.provider :libvirt do |libvirt|
libvirt.cpus = 3
libvirt.memory = 8192
end
# This uses the vagrant-hostsupdater plugin, and lets you # This uses the vagrant-hostsupdater plugin, and lets you
# access the development site at http://mastodon.local. # access the development site at http://mastodon.local.
# If you change it, also change it in .env.vagrant before provisioning # If you change it, also change it in .env.vagrant before provisioning

View file

@ -4,6 +4,18 @@ module Admin
class DomainBlocksController < BaseController class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:destroy, :edit, :update] before_action :set_domain_block, only: [:destroy, :edit, :update]
PERMITTED_PARAMS = %i(
domain
obfuscate
private_comment
public_comment
reject_media
reject_reports
severity
).freeze
PERMITTED_UPDATE_PARAMS = PERMITTED_PARAMS.without(:domain).freeze
def batch def batch
authorize :domain_block, :create? authorize :domain_block, :create?
@form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) @form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button))
@ -88,11 +100,17 @@ module Admin
end end
def update_params def update_params
params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) params
.require(:domain_block)
.slice(*PERMITTED_UPDATE_PARAMS)
.permit(*PERMITTED_UPDATE_PARAMS)
end end
def resource_params def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) params
.require(:domain_block)
.slice(*PERMITTED_PARAMS)
.permit(*PERMITTED_PARAMS)
end end
def form_domain_block_batch_params def form_domain_block_batch_params

View file

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

View file

@ -13,6 +13,13 @@ class Api::V1::Admin::TagsController < Api::BaseController
LIMIT = 100 LIMIT = 100
PERMITTED_PARAMS = %i(
display_name
listable
trendable
usable
).freeze
def index def index
authorize :tag, :index? authorize :tag, :index?
render json: @tags, each_serializer: REST::Admin::TagSerializer render json: @tags, each_serializer: REST::Admin::TagSerializer
@ -40,7 +47,9 @@ class Api::V1::Admin::TagsController < Api::BaseController
end end
def tag_params def tag_params
params.permit(:display_name, :trendable, :usable, :listable) params
.slice(*PERMITTED_PARAMS)
.permit(*PERMITTED_PARAMS)
end end
def next_path def next_path

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
before_action :set_preview_card
before_action :set_statuses
PERMITTED_PARAMS = %i(
url
limit
).freeze
def show
cache_if_unauthenticated!
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
def require_auth?
!Setting.timeline_preview
end
def set_preview_card
@preview_card = PreviewCard.joins(:trend).merge(PreviewCardTrend.allowed).find_by!(url: params[:url])
end
def set_statuses
@statuses = @preview_card.nil? ? [] : preload_collection(link_timeline_statuses, Status)
end
def link_timeline_statuses
link_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id],
params[:min_id]
)
end
def link_feed
LinkFeed.new(@preview_card, current_account)
end
def next_path
api_v1_timelines_link_url next_path_params
end
def prev_path
api_v1_timelines_link_url prev_path_params
end
end

View file

@ -0,0 +1,91 @@
# frozen_string_literal: true
class Api::V2Alpha::NotificationsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss]
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss]
before_action :require_user!
after_action :insert_pagination_headers, only: :index
DEFAULT_NOTIFICATIONS_LIMIT = 40
def index
with_read_replica do
@notifications = load_notifications
@group_metadata = load_group_metadata
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
end
render json: @notifications.map { |notification| NotificationGroup.from_notification(notification) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata
end
def show
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id])
render json: NotificationGroup.from_notification(@notification), serializer: REST::NotificationGroupSerializer
end
def clear
current_account.notifications.delete_all
render_empty
end
def dismiss
current_account.notifications.where(group_key: params[:id]).destroy_all
render_empty
end
private
def load_notifications
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
preload_collection(target_statuses, Status)
end
end
def load_group_metadata
return {} if @notifications.empty?
browserable_account_notifications
.where(group_key: @notifications.filter_map(&:group_key))
.where(id: (@notifications.last.id)..(@notifications.first.id))
.group(:group_key)
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
end
def browserable_account_notifications
current_account.notifications.without_suspended.browserable(
types: Array(browserable_params[:types]),
exclude_types: Array(browserable_params[:exclude_types]),
include_filtered: truthy_param?(:include_filtered)
)
end
def target_statuses_from_notifications
@notifications.filter_map(&:target_status)
end
def next_path
api_v2_alpha_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty?
end
def prev_path
api_v2_alpha_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
end
def pagination_collection
@notifications
end
def browserable_params
params.permit(:include_filtered, types: [], exclude_types: [])
end
def pagination_params(core_params)
params.slice(:limit, :types, :exclude_types, :include_filtered).permit(:limit, :include_filtered, types: [], exclude_types: []).merge(core_params)
end
end

View file

@ -13,7 +13,7 @@ class Settings::ApplicationsController < Settings::BaseController
def new def new
@application = Doorkeeper::Application.new( @application = Doorkeeper::Application.new(
redirect_uri: Doorkeeper.configuration.native_redirect_uri, redirect_uri: Doorkeeper.configuration.native_redirect_uri,
scopes: 'read:me' scopes: 'profile'
) )
end end

View file

@ -15,15 +15,15 @@ module Admin::ActionLogsHelper
link_to log.human_identifier, admin_roles_path(log.target_id) link_to log.human_identifier, admin_roles_path(log.target_id)
when 'Report' when 'Report'
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id) link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
link_to log.human_identifier, "https://#{log.human_identifier.presence}" log.human_identifier.present? ? link_to(log.human_identifier, admin_instance_path(log.human_identifier)) : I18n.t('admin.action_logs.unavailable_instance')
when 'Status' when 'Status'
link_to log.human_identifier, log.permalink link_to log.human_identifier, log.permalink
when 'AccountWarning' when 'AccountWarning'
link_to log.human_identifier, disputes_strike_path(log.target_id) link_to log.human_identifier, disputes_strike_path(log.target_id)
when 'Announcement' when 'Announcement'
link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id) link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id)
when 'IpBlock', 'Instance', 'CustomEmoji' when 'IpBlock', 'EmailDomainBlock', 'CustomEmoji'
log.human_identifier log.human_identifier
when 'CanonicalEmailBlock' when 'CanonicalEmailBlock'
content_tag(:samp, (log.human_identifier.presence || '')[0...7], title: log.human_identifier) content_tag(:samp, (log.human_identifier.presence || '')[0...7], title: log.human_identifier)

View file

@ -68,13 +68,17 @@ export function importFetchedStatuses(statuses) {
status.filtered.forEach(result => pushUnique(filters, result.filter)); status.filtered.forEach(result => pushUnique(filters, result.filter));
} }
if (status.reblog && status.reblog.id) { if (status.reblog?.id) {
processStatus(status.reblog); processStatus(status.reblog);
} }
if (status.poll && status.poll.id) { if (status.poll?.id) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
} }
if (status.card?.author_account) {
pushUnique(accounts, status.card.author_account);
}
} }
statuses.forEach(processStatus); statuses.forEach(processStatus);

View file

@ -36,6 +36,10 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.poll = status.poll.id; normalStatus.poll = status.poll.id;
} }
if (status.card?.author_account) {
normalStatus.card = { ...status.card, author_account: status.card.author_account.id };
}
if (status.filtered) { if (status.filtered) {
normalStatus.filtered = status.filtered.map(normalizeFilterResult); normalStatus.filtered = status.filtered.map(normalizeFilterResult);
} }

View file

@ -1,6 +1,6 @@
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { importFetchedStatuses } from './importer'; import { importFetchedStatuses, importFetchedAccounts } from './importer';
export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS'; export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS';
@ -49,8 +49,11 @@ export const fetchTrendingLinks = () => (dispatch) => {
dispatch(fetchTrendingLinksRequest()); dispatch(fetchTrendingLinksRequest());
api() api()
.get('/api/v1/trends/links') .get('/api/v1/trends/links', { params: { limit: 20 } })
.then(({ data }) => dispatch(fetchTrendingLinksSuccess(data))) .then(({ data }) => {
dispatch(importFetchedAccounts(data.map(link => link.author_account).filter(account => !!account)));
dispatch(fetchTrendingLinksSuccess(data));
})
.catch(err => dispatch(fetchTrendingLinksFail(err))); .catch(err => dispatch(fetchTrendingLinksFail(err)));
}; };

View file

@ -0,0 +1,19 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { AuthorLink } from 'mastodon/features/explore/components/author_link';
export const MoreFromAuthor = ({ accountId }) => (
<div className='more-from-author'>
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
<use xlinkHref='#logo-symbol-icon' />
</svg>
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
</div>
);
MoreFromAuthor.propTypes = {
accountId: PropTypes.string.isRequired,
};

View file

@ -42,10 +42,12 @@ class ServerBanner extends PureComponent {
return ( return (
<div className='server-banner'> <div className='server-banner'>
<div className='server-banner__introduction'> <div className='server-banner__introduction'>
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} /> <FormattedMessage id='server_banner.is_one_of_many' defaultMessage='{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
</div> </div>
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' /> <Link to='/about'>
<ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
</Link>
<div className='server-banner__description'> <div className='server-banner__description'>
{isLoading ? ( {isLoading ? (
@ -84,10 +86,6 @@ class ServerBanner extends PureComponent {
)} )}
</div> </div>
</div> </div>
<hr className='spacer' />
<Link className='button button--block button-secondary' to='/about'><FormattedMessage id='server_banner.learn_more' defaultMessage='Learn more' /></Link>
</div> </div>
); );
} }

View file

@ -199,6 +199,7 @@ class AccountTimeline extends ImmutablePureComponent {
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
timelineId='account' timelineId='account'
withCounters
/> />
</Column> </Column>
); );

View file

@ -110,18 +110,6 @@ class LanguageDropdownMenu extends PureComponent {
}).map(result => result.obj); }).map(result => result.obj);
} }
frequentlyUsed () {
const { languages, value } = this.props;
const current = languages.find(lang => lang[0] === value);
const results = [];
if (current) {
results.push(current);
}
return results;
}
handleClick = e => { handleClick = e => {
const value = e.currentTarget.getAttribute('data-index'); const value = e.currentTarget.getAttribute('data-index');

View file

@ -0,0 +1,21 @@
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Avatar } from 'mastodon/components/avatar';
import { useAppSelector } from 'mastodon/store';
export const AuthorLink = ({ accountId }) => {
const account = useAppSelector(state => state.getIn(['accounts', accountId]));
return (
<Link to={`/@${account.get('acct')}`} className='story__details__shared__author-link'>
<Avatar account={account} size={16} />
<bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} />
</Link>
);
};
AuthorLink.propTypes = {
accountId: PropTypes.string.isRequired,
};

View file

@ -1,61 +1,89 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { useState, useCallback } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Blurhash } from 'mastodon/components/blurhash'; import { Blurhash } from 'mastodon/components/blurhash';
import { accountsCountRenderer } from 'mastodon/components/hashtag';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { ShortNumber } from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton'; import { Skeleton } from 'mastodon/components/skeleton';
export default class Story extends PureComponent { import { AuthorLink } from './author_link';
static propTypes = { const sharesCountRenderer = (displayNumber, pluralReady) => (
url: PropTypes.string, <FormattedMessage
title: PropTypes.string, id='link_preview.shares'
lang: PropTypes.string, defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
publisher: PropTypes.string, values={{
publishedAt: PropTypes.string, count: pluralReady,
author: PropTypes.string, counter: <strong>{displayNumber}</strong>,
sharedTimes: PropTypes.number, }}
thumbnail: PropTypes.string, />
thumbnailDescription: PropTypes.string, );
blurhash: PropTypes.string,
expanded: PropTypes.bool,
};
state = { export const Story = ({
thumbnailLoaded: false, url,
}; title,
lang,
publisher,
publishedAt,
author,
authorAccount,
sharedTimes,
thumbnail,
thumbnailDescription,
blurhash,
expanded
}) => {
const [thumbnailLoaded, setThumbnailLoaded] = useState(false);
handleImageLoad = () => this.setState({ thumbnailLoaded: true }); const handleImageLoad = useCallback(() => {
setThumbnailLoaded(true);
}, [setThumbnailLoaded]);
render () { return (
const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props; <div className={classNames('story', { expanded })}>
<div className='story__details'>
const { thumbnailLoaded } = this.state; <div className='story__details__publisher'>
{publisher ? <span lang={lang}>{publisher}</span> : <Skeleton width={50} />}{publishedAt && <> · <RelativeTimestamp timestamp={publishedAt} /></>}
return (
<a className={classNames('story', { expanded })} href={url} target='blank' rel='noopener'>
<div className='story__details'>
<div className='story__details__publisher'>{publisher ? <span lang={lang}>{publisher}</span> : <Skeleton width={50} />}{publishedAt && <> · <RelativeTimestamp timestamp={publishedAt} /></>}</div>
<div className='story__details__title' lang={lang}>{title ? title : <Skeleton />}</div>
<div className='story__details__shared'>{author && <><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{author}</strong> }} /> · </>}{typeof sharedTimes === 'number' ? <ShortNumber value={sharedTimes} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}</div>
</div> </div>
<div className='story__thumbnail'> <a className='story__details__title' lang={lang} href={url} target='blank' rel='noopener'>
{thumbnail ? ( {title ? title : <Skeleton />}
<> </a>
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
<img src={thumbnail} onLoad={this.handleImageLoad} alt={thumbnailDescription} title={thumbnailDescription} lang={lang} /> <div className='story__details__shared'>
</> {author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
) : <Skeleton />} {typeof sharedTimes === 'number' ? <span className='story__details__shared__pill'><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></span> : <Skeleton width='10ch' />}
</div> </div>
</div>
<a className='story__thumbnail' href={url} target='blank' rel='noopener'>
{thumbnail ? (
<>
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
<img src={thumbnail} onLoad={handleImageLoad} alt={thumbnailDescription} title={thumbnailDescription} lang={lang} />
</>
) : <Skeleton />}
</a> </a>
); </div>
} );
};
} Story.propTypes = {
url: PropTypes.string,
title: PropTypes.string,
lang: PropTypes.string,
publisher: PropTypes.string,
publishedAt: PropTypes.string,
author: PropTypes.string,
authorAccount: PropTypes.string,
sharedTimes: PropTypes.number,
thumbnail: PropTypes.string,
thumbnailDescription: PropTypes.string,
blurhash: PropTypes.string,
expanded: PropTypes.bool,
};

View file

@ -13,7 +13,7 @@ import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import Story from './components/story'; import { Story } from './components/story';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
links: state.getIn(['trends', 'links', 'items']), links: state.getIn(['trends', 'links', 'items']),
@ -75,6 +75,7 @@ class Links extends PureComponent {
publisher={link.get('provider_name')} publisher={link.get('provider_name')}
publishedAt={link.get('published_at')} publishedAt={link.get('published_at')}
author={link.get('author_name')} author={link.get('author_name')}
authorAccount={link.getIn(['author_account', 'id'])}
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1} sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
thumbnail={link.get('image')} thumbnail={link.get('image')}
thumbnailDescription={link.get('image_description')} thumbnailDescription={link.get('image_description')}

View file

@ -6,7 +6,6 @@ import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom';
import Immutable from 'immutable'; import Immutable from 'immutable';
@ -15,9 +14,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react'; import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react'; import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react'; import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
import { Avatar } from 'mastodon/components/avatar';
import { Blurhash } from 'mastodon/components/blurhash'; import { Blurhash } from 'mastodon/components/blurhash';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { MoreFromAuthor } from 'mastodon/components/more_from_author';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { useBlurhash } from 'mastodon/initial_state'; import { useBlurhash } from 'mastodon/initial_state';
@ -59,20 +58,6 @@ const addAutoPlay = html => {
return html; return html;
}; };
const MoreFromAuthor = ({ author }) => (
<div className='more-from-author'>
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
<use xlinkHref='#logo-symbol-icon' />
</svg>
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <Link to={`/@${author.get('acct')}`}><Avatar account={author} size={16} /> {author.get('display_name')}</Link> }} />
</div>
);
MoreFromAuthor.propTypes = {
author: ImmutablePropTypes.map,
};
export default class Card extends PureComponent { export default class Card extends PureComponent {
static propTypes = { static propTypes = {
@ -259,7 +244,7 @@ export default class Card extends PureComponent {
{description} {description}
</a> </a>
{showAuthor && <MoreFromAuthor author={card.get('author_account')} />} {showAuthor && <MoreFromAuthor accountId={card.get('author_account')} />}
</> </>
); );
} }

View file

@ -22,7 +22,8 @@ const SignInBanner = () => {
if (sso_redirect) { if (sso_redirect) {
return ( return (
<div className='sign-in-banner'> <div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p> <p><strong><FormattedMessage id='sign_in_banner.mastodon_is' defaultMessage="Mastodon is the best way to keep up with what's happening." /></strong></p>
<p><FormattedMessage id='sign_in_banner.follow_anyone' defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.' /></p>
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a> <a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
</div> </div>
); );
@ -44,7 +45,8 @@ const SignInBanner = () => {
return ( return (
<div className='sign-in-banner'> <div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.' /></p> <p><strong><FormattedMessage id='sign_in_banner.mastodon_is' defaultMessage="Mastodon is the best way to keep up with what's happening." /></strong></p>
<p><FormattedMessage id='sign_in_banner.follow_anyone' defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.' /></p>
{signupButton} {signupButton}
<a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a> <a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</div> </div>

View file

@ -225,7 +225,11 @@
"domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.", "domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.",
"domain_pill.username": "اسم المستخدم", "domain_pill.username": "اسم المستخدم",
"domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟", "domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟",
"domain_pill.who_they_are": "بما أن المعالجات تقول من هو الشخص ومكان وجوده، يمكنك التفاعل مع الناس عبر الشبكة الاجتماعية لـ <button>ActivityPub-Power منصات</button>.",
"domain_pill.who_you_are": "لأن معالجتك تقول من أنت ومكان وجودك، يمكن الناس التفاعل معك عبر الشبكة الاجتماعية لـ <button>ActivityPub-Power منصات</button>.",
"domain_pill.your_handle": "عنوانك الكامل:", "domain_pill.your_handle": "عنوانك الكامل:",
"domain_pill.your_server": "منزلك الرقمي، حيث تعيش جميع مشاركاتك. لا تحب هذا؟ إنقل الخوادم في أي وقت واخضر متابعينك أيضًا.",
"domain_pill.your_username": "معرفك الفريد على هذا الخادم. من الممكن العثور على مستخدمين بنفس إسم المستخدم على خوادم مختلفة.",
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.", "embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
"embed.preview": "إليك ما سيبدو عليه:", "embed.preview": "إليك ما سيبدو عليه:",
"emoji_button.activity": "الأنشطة", "emoji_button.activity": "الأنشطة",
@ -262,6 +266,7 @@
"empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر منشورات.", "empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر منشورات.",
"empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قوائمك هنا إن قمت بإنشاء واحدة.", "empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قوائمك هنا إن قمت بإنشاء واحدة.",
"empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.", "empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.",
"empty_column.notification_requests": "لا يوجد شيء هنا. عندما تتلقى إشعارات جديدة، سوف تظهر هنا وفقًا لإعداداتك.",
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
"empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات", "empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات",
"error.unexpected_crash.explanation": "نظرا لوجود خطأ في التعليمات البرمجية أو مشكلة توافق مع المتصفّح، تعذر عرض هذه الصفحة بشكل صحيح.", "error.unexpected_crash.explanation": "نظرا لوجود خطأ في التعليمات البرمجية أو مشكلة توافق مع المتصفّح، تعذر عرض هذه الصفحة بشكل صحيح.",
@ -292,6 +297,8 @@
"filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة", "filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة",
"filter_modal.select_filter.title": "تصفية هذا المنشور", "filter_modal.select_filter.title": "تصفية هذا المنشور",
"filter_modal.title.status": "تصفية منشور", "filter_modal.title.status": "تصفية منشور",
"filtered_notifications_banner.mentions": "{count, plural, one {إشارة} two {إشارتين} few {# إشارات} other {# إشارة}}",
"filtered_notifications_banner.pending_requests": "إشعارات من {count, plural, zero {}=0 {لا أحد} one {شخص واحد قد تعرفه} two {شخصين قد تعرفهما} few {# أشخاص قد تعرفهم} many {# شخص قد تعرفهم} other {# شخص قد تعرفهم}}",
"filtered_notifications_banner.title": "الإشعارات المصفاة", "filtered_notifications_banner.title": "الإشعارات المصفاة",
"firehose.all": "الكل", "firehose.all": "الكل",
"firehose.local": "هذا الخادم", "firehose.local": "هذا الخادم",
@ -301,6 +308,8 @@
"follow_requests.unlocked_explanation": "حتى وإن كان حسابك غير مقفل، يعتقد فريق {domain} أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.", "follow_requests.unlocked_explanation": "حتى وإن كان حسابك غير مقفل، يعتقد فريق {domain} أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.",
"follow_suggestions.curated_suggestion": "اختيار الموظفين", "follow_suggestions.curated_suggestion": "اختيار الموظفين",
"follow_suggestions.dismiss": "لا تُظهرها مجدّدًا", "follow_suggestions.dismiss": "لا تُظهرها مجدّدًا",
"follow_suggestions.featured_longer": "مختار يدوياً من قِبل فريق {domain}",
"follow_suggestions.friends_of_friends_longer": "مشهور بين الأشخاص الذين تتابعهم",
"follow_suggestions.hints.featured": "تم اختيار هذا الملف الشخصي يدوياً من قبل فريق {domain}.", "follow_suggestions.hints.featured": "تم اختيار هذا الملف الشخصي يدوياً من قبل فريق {domain}.",
"follow_suggestions.hints.friends_of_friends": "هذا الملف الشخصي مشهور بين الأشخاص الذين تتابعهم.", "follow_suggestions.hints.friends_of_friends": "هذا الملف الشخصي مشهور بين الأشخاص الذين تتابعهم.",
"follow_suggestions.hints.most_followed": "هذا الملف الشخصي هو واحد من الأكثر متابعة على {domain}.", "follow_suggestions.hints.most_followed": "هذا الملف الشخصي هو واحد من الأكثر متابعة على {domain}.",
@ -405,6 +414,7 @@
"limited_account_hint.action": "إظهار الملف التعريفي على أي حال", "limited_account_hint.action": "إظهار الملف التعريفي على أي حال",
"limited_account_hint.title": "تم إخفاء هذا الملف الشخصي من قبل مشرفي {domain}.", "limited_account_hint.title": "تم إخفاء هذا الملف الشخصي من قبل مشرفي {domain}.",
"link_preview.author": "مِن {name}", "link_preview.author": "مِن {name}",
"link_preview.more_from_author": "المزيد من {name}",
"lists.account.add": "أضف إلى القائمة", "lists.account.add": "أضف إلى القائمة",
"lists.account.remove": "احذف من القائمة", "lists.account.remove": "احذف من القائمة",
"lists.delete": "احذف القائمة", "lists.delete": "احذف القائمة",
@ -465,10 +475,13 @@
"notification.follow_request": "لقد طلب {name} متابعتك", "notification.follow_request": "لقد طلب {name} متابعتك",
"notification.mention": "{name} ذكرك", "notification.mention": "{name} ذكرك",
"notification.moderation-warning.learn_more": "اعرف المزيد", "notification.moderation-warning.learn_more": "اعرف المزيد",
"notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف",
"notification.moderation_warning.action_delete_statuses": "تم إزالة بعض مشاركاتك.",
"notification.moderation_warning.action_disable": "تم تعطيل حسابك.", "notification.moderation_warning.action_disable": "تم تعطيل حسابك.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "بعض من منشوراتك تم تصنيفها على أنها حساسة.", "notification.moderation_warning.action_mark_statuses_as_sensitive": "بعض من منشوراتك تم تصنيفها على أنها حساسة.",
"notification.moderation_warning.action_none": "لقد تلقى حسابك تحذيرا بالإشراف.", "notification.moderation_warning.action_none": "لقد تلقى حسابك تحذيرا بالإشراف.",
"notification.moderation_warning.action_sensitive": "سيتم وضع علامة على منشوراتك على أنها حساسة من الآن فصاعدا.", "notification.moderation_warning.action_sensitive": "سيتم وضع علامة على منشوراتك على أنها حساسة من الآن فصاعدا.",
"notification.moderation_warning.action_silence": "لقد تم تقييد حسابك.",
"notification.moderation_warning.action_suspend": "لقد تم تعليق حسابك.", "notification.moderation_warning.action_suspend": "لقد تم تعليق حسابك.",
"notification.own_poll": "انتهى استطلاعك للرأي", "notification.own_poll": "انتهى استطلاعك للرأي",
"notification.poll": "لقد انتهى استطلاع رأي شاركتَ فيه", "notification.poll": "لقد انتهى استطلاع رأي شاركتَ فيه",

View file

@ -308,6 +308,8 @@
"follow_requests.unlocked_explanation": "Ваш акаўнт не схаваны, аднак прадстаўнікі {domain} палічылі, што вы можаце захацець праглядзець запыты на падпіску з гэтых профіляў уручную.", "follow_requests.unlocked_explanation": "Ваш акаўнт не схаваны, аднак прадстаўнікі {domain} палічылі, што вы можаце захацець праглядзець запыты на падпіску з гэтых профіляў уручную.",
"follow_suggestions.curated_suggestion": "Выбар адміністрацыі", "follow_suggestions.curated_suggestion": "Выбар адміністрацыі",
"follow_suggestions.dismiss": "Не паказваць зноў", "follow_suggestions.dismiss": "Не паказваць зноў",
"follow_suggestions.featured_longer": "Адабраныя камандай {domain} уручную",
"follow_suggestions.friends_of_friends_longer": "Папулярнае сярод людзей, на якіх Вы падпісаны",
"follow_suggestions.hints.featured": "Гэты профіль быў выбраны ўручную камандай {domain}.", "follow_suggestions.hints.featured": "Гэты профіль быў выбраны ўручную камандай {domain}.",
"follow_suggestions.hints.friends_of_friends": "Гэты профіль папулярны сярод людзей, на якіх вы падпісаліся.", "follow_suggestions.hints.friends_of_friends": "Гэты профіль папулярны сярод людзей, на якіх вы падпісаліся.",
"follow_suggestions.hints.most_followed": "Гэты профіль - адзін з профіляў з самай вялікай колькасцю падпісак на {domain}.", "follow_suggestions.hints.most_followed": "Гэты профіль - адзін з профіляў з самай вялікай колькасцю падпісак на {domain}.",
@ -315,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "Гэты профіль падобны на профілі, на якія вы нядаўна падпісаліся.", "follow_suggestions.hints.similar_to_recently_followed": "Гэты профіль падобны на профілі, на якія вы нядаўна падпісаліся.",
"follow_suggestions.personalized_suggestion": "Персаналізаваная прапанова", "follow_suggestions.personalized_suggestion": "Персаналізаваная прапанова",
"follow_suggestions.popular_suggestion": "Папулярная прапанова", "follow_suggestions.popular_suggestion": "Папулярная прапанова",
"follow_suggestions.popular_suggestion_longer": "Папулярнае на {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Падобныя профілі, за якімі вы нядаўна сачылі",
"follow_suggestions.view_all": "Праглядзець усё", "follow_suggestions.view_all": "Праглядзець усё",
"follow_suggestions.who_to_follow": "На каго падпісацца", "follow_suggestions.who_to_follow": "На каго падпісацца",
"followed_tags": "Падпіскі", "followed_tags": "Падпіскі",
@ -410,6 +414,7 @@
"limited_account_hint.action": "Усе роўна паказваць профіль", "limited_account_hint.action": "Усе роўна паказваць профіль",
"limited_account_hint.title": "Гэты профіль быў схаваны мадэратарамі", "limited_account_hint.title": "Гэты профіль быў схаваны мадэратарамі",
"link_preview.author": "Ад {name}", "link_preview.author": "Ад {name}",
"link_preview.more_from_author": "Больш ад {name}",
"lists.account.add": "Дадаць да спісу", "lists.account.add": "Дадаць да спісу",
"lists.account.remove": "Выдаліць са спісу", "lists.account.remove": "Выдаліць са спісу",
"lists.delete": "Выдаліць спіс", "lists.delete": "Выдаліць спіс",
@ -458,7 +463,7 @@
"navigation_bar.opened_in_classic_interface": "Допісы, уліковыя запісы і іншыя спецыфічныя старонкі па змоўчанні адчыняюцца ў класічным вэб-інтэрфейсе.", "navigation_bar.opened_in_classic_interface": "Допісы, уліковыя запісы і іншыя спецыфічныя старонкі па змоўчанні адчыняюцца ў класічным вэб-інтэрфейсе.",
"navigation_bar.personal": "Асабістае", "navigation_bar.personal": "Асабістае",
"navigation_bar.pins": "Замацаваныя допісы", "navigation_bar.pins": "Замацаваныя допісы",
"navigation_bar.preferences": "Параметры", "navigation_bar.preferences": "Налады",
"navigation_bar.public_timeline": "Глабальная стужка", "navigation_bar.public_timeline": "Глабальная стужка",
"navigation_bar.search": "Пошук", "navigation_bar.search": "Пошук",
"navigation_bar.security": "Бяспека", "navigation_bar.security": "Бяспека",
@ -470,10 +475,22 @@
"notification.follow_request": "{name} адправіў запыт на падпіску", "notification.follow_request": "{name} адправіў запыт на падпіску",
"notification.mention": "{name} згадаў вас", "notification.mention": "{name} згадаў вас",
"notification.moderation-warning.learn_more": "Даведацца больш", "notification.moderation-warning.learn_more": "Даведацца больш",
"notification.moderation_warning": "Вы атрымалі папярэджанне аб мадэрацыі",
"notification.moderation_warning.action_delete_statuses": "Некаторыя вашыя допісы былі выдаленыя.",
"notification.moderation_warning.action_disable": "Ваш уліковы запіс быў адключаны.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Некаторыя з вашых допісаў былі пазначаныя як далікатныя.",
"notification.moderation_warning.action_none": "Ваш уліковы запіс атрымаў папярэджанне ад мадэратараў.",
"notification.moderation_warning.action_sensitive": "З гэтага моманту вашыя допісы будуць пазначаныя як далікатныя.",
"notification.moderation_warning.action_silence": "Ваш уліковы запіс быў абмежаваны.",
"notification.moderation_warning.action_suspend": "Ваш уліковы запіс быў прыпынены.",
"notification.own_poll": "Ваша апытанне скончылася", "notification.own_poll": "Ваша апытанне скончылася",
"notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася", "notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася",
"notification.reblog": "{name} пашырыў ваш допіс", "notification.reblog": "{name} пашырыў ваш допіс",
"notification.relationships_severance_event": "Страціў сувязь з {name}",
"notification.relationships_severance_event.account_suspension": "Адміністратар з {from} прыпыніў працу {target}, што азначае, што вы больш не можаце атрымліваць ад іх абнаўлення ці ўзаемадзейнічаць з імі.",
"notification.relationships_severance_event.domain_block": "Адміністратар з {from} заблакіраваў {target}, у тым ліку {followersCount} вашых падпісчыка(-аў) і {followingCount, plural, one {# уліковы запіс} few {# уліковыя запісы} many {# уліковых запісаў} other {# уліковых запісаў}}.",
"notification.relationships_severance_event.learn_more": "Даведацца больш", "notification.relationships_severance_event.learn_more": "Даведацца больш",
"notification.relationships_severance_event.user_domain_block": "Вы заблакіравалі {target} выдаліўшы {followersCount} сваіх падпісчыкаў і {followingCount, plural, one {# уліковы запіс} few {# уліковыя запісы} many {# уліковых запісаў} other {# уліковых запісаў}}, за якімі вы сочыце.",
"notification.status": "Новы допіс ад {name}", "notification.status": "Новы допіс ад {name}",
"notification.update": "Допіс {name} адрэдагаваны", "notification.update": "Допіс {name} адрэдагаваны",
"notification_requests.accept": "Прыняць", "notification_requests.accept": "Прыняць",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Aquest perfil l'han amagat els moderadors de {domain}.", "limited_account_hint.title": "Aquest perfil l'han amagat els moderadors de {domain}.",
"link_preview.author": "Per {name}", "link_preview.author": "Per {name}",
"link_preview.more_from_author": "Més de {name}", "link_preview.more_from_author": "Més de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicació} other {{counter} publicacions}}",
"lists.account.add": "Afegeix a la llista", "lists.account.add": "Afegeix a la llista",
"lists.account.remove": "Elimina de la llista", "lists.account.remove": "Elimina de la llista",
"lists.delete": "Elimina la llista", "lists.delete": "Elimina la llista",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Denne profil er blevet skjult af {domain}-moderatorerne.", "limited_account_hint.title": "Denne profil er blevet skjult af {domain}-moderatorerne.",
"link_preview.author": "Af {name}", "link_preview.author": "Af {name}",
"link_preview.more_from_author": "Mere fra {name}", "link_preview.more_from_author": "Mere fra {name}",
"link_preview.shares": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}",
"lists.account.add": "Føj til liste", "lists.account.add": "Føj til liste",
"lists.account.remove": "Fjern fra liste", "lists.account.remove": "Fjern fra liste",
"lists.delete": "Slet liste", "lists.delete": "Slet liste",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Dieses Profil wurde von den Moderator*innen von {domain} ausgeblendet.", "limited_account_hint.title": "Dieses Profil wurde von den Moderator*innen von {domain} ausgeblendet.",
"link_preview.author": "Von {name}", "link_preview.author": "Von {name}",
"link_preview.more_from_author": "Mehr von {name}", "link_preview.more_from_author": "Mehr von {name}",
"link_preview.shares": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
"lists.account.add": "Zur Liste hinzufügen", "lists.account.add": "Zur Liste hinzufügen",
"lists.account.remove": "Von der Liste entfernen", "lists.account.remove": "Von der Liste entfernen",
"lists.delete": "Liste löschen", "lists.delete": "Liste löschen",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.", "limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
"link_preview.author": "By {name}", "link_preview.author": "By {name}",
"link_preview.more_from_author": "More from {name}", "link_preview.more_from_author": "More from {name}",
"link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}",
"lists.account.add": "Add to list", "lists.account.add": "Add to list",
"lists.account.remove": "Remove from list", "lists.account.remove": "Remove from list",
"lists.delete": "Delete list", "lists.delete": "Delete list",
@ -695,13 +696,13 @@
"server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)",
"server_banner.active_users": "active users", "server_banner.active_users": "active users",
"server_banner.administered_by": "Administered by:", "server_banner.administered_by": "Administered by:",
"server_banner.introduction": "{domain} is part of the decentralized social network powered by {mastodon}.", "server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.",
"server_banner.learn_more": "Learn more",
"server_banner.server_stats": "Server stats:", "server_banner.server_stats": "Server stats:",
"sign_in_banner.create_account": "Create account", "sign_in_banner.create_account": "Create account",
"sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.",
"sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.",
"sign_in_banner.sign_in": "Login", "sign_in_banner.sign_in": "Login",
"sign_in_banner.sso_redirect": "Login or Register", "sign_in_banner.sso_redirect": "Login or Register",
"sign_in_banner.text": "Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.",
"status.admin_account": "Open moderation interface for @{name}", "status.admin_account": "Open moderation interface for @{name}",
"status.admin_domain": "Open moderation interface for {domain}", "status.admin_domain": "Open moderation interface for {domain}",
"status.admin_status": "Open this post in the moderation interface", "status.admin_status": "Open this post in the moderation interface",

View file

@ -498,7 +498,14 @@
"poll_button.add_poll": "Aldoni balotenketon", "poll_button.add_poll": "Aldoni balotenketon",
"poll_button.remove_poll": "Forigi balotenketon", "poll_button.remove_poll": "Forigi balotenketon",
"privacy.change": "Agordi mesaĝan privatecon", "privacy.change": "Agordi mesaĝan privatecon",
"privacy.direct.long": "Ĉiuj menciitaj en la afiŝo",
"privacy.direct.short": "Specifaj homoj",
"privacy.private.long": "Nur viaj sekvantoj",
"privacy.private.short": "Sekvantoj",
"privacy.public.long": "Ĉiujn ajn ĉe kaj ekster Mastodon",
"privacy.public.short": "Publika", "privacy.public.short": "Publika",
"privacy.unlisted.long": "Malpli algoritmaj fanfaroj",
"privacy.unlisted.short": "Diskrete publika",
"privacy_policy.last_updated": "Laste ĝisdatigita en {date}", "privacy_policy.last_updated": "Laste ĝisdatigita en {date}",
"privacy_policy.title": "Politiko de privateco", "privacy_policy.title": "Politiko de privateco",
"recommended": "Rekomendita", "recommended": "Rekomendita",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Este perfil fue ocultado por los moderadores de {domain}.", "limited_account_hint.title": "Este perfil fue ocultado por los moderadores de {domain}.",
"link_preview.author": "Por {name}", "link_preview.author": "Por {name}",
"link_preview.more_from_author": "Más de {name}", "link_preview.more_from_author": "Más de {name}",
"link_preview.shares": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}",
"lists.account.add": "Agregar a lista", "lists.account.add": "Agregar a lista",
"lists.account.remove": "Quitar de lista", "lists.account.remove": "Quitar de lista",
"lists.delete": "Eliminar lista", "lists.delete": "Eliminar lista",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.", "limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.",
"link_preview.author": "Por {name}", "link_preview.author": "Por {name}",
"link_preview.more_from_author": "Más de {name}", "link_preview.more_from_author": "Más de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
"lists.account.add": "Añadir a lista", "lists.account.add": "Añadir a lista",
"lists.account.remove": "Quitar de lista", "lists.account.remove": "Quitar de lista",
"lists.delete": "Borrar lista", "lists.delete": "Borrar lista",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.", "limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.",
"link_preview.author": "Por {name}", "link_preview.author": "Por {name}",
"link_preview.more_from_author": "Más de {name}", "link_preview.more_from_author": "Más de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}",
"lists.account.add": "Añadir a lista", "lists.account.add": "Añadir a lista",
"lists.account.remove": "Quitar de lista", "lists.account.remove": "Quitar de lista",
"lists.delete": "Borrar lista", "lists.delete": "Borrar lista",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Palvelimen {domain} valvojat ovat piilottaneet tämän käyttäjätilin.", "limited_account_hint.title": "Palvelimen {domain} valvojat ovat piilottaneet tämän käyttäjätilin.",
"link_preview.author": "Julkaissut {name}", "link_preview.author": "Julkaissut {name}",
"link_preview.more_from_author": "Lisää käyttäjältä {name}", "link_preview.more_from_author": "Lisää käyttäjältä {name}",
"link_preview.shares": "{count, plural, one {{counter} julkaisu} other {{counter} julkaisua}}",
"lists.account.add": "Lisää listalle", "lists.account.add": "Lisää listalle",
"lists.account.remove": "Poista listalta", "lists.account.remove": "Poista listalta",
"lists.delete": "Poista lista", "lists.delete": "Poista lista",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Hesin vangin er fjaldur av kjakleiðarunum á {domain}.", "limited_account_hint.title": "Hesin vangin er fjaldur av kjakleiðarunum á {domain}.",
"link_preview.author": "Av {name}", "link_preview.author": "Av {name}",
"link_preview.more_from_author": "Meira frá {name}", "link_preview.more_from_author": "Meira frá {name}",
"link_preview.shares": "{count, plural, one {{counter} postur} other {{counter} postar}}",
"lists.account.add": "Legg afturat lista", "lists.account.add": "Legg afturat lista",
"lists.account.remove": "Tak av lista", "lists.account.remove": "Tak av lista",
"lists.delete": "Strika lista", "lists.delete": "Strika lista",

View file

@ -156,7 +156,7 @@
"compose_form.poll.duration": "Durée du sondage", "compose_form.poll.duration": "Durée du sondage",
"compose_form.poll.multiple": "Choix multiple", "compose_form.poll.multiple": "Choix multiple",
"compose_form.poll.option_placeholder": "Option {number}", "compose_form.poll.option_placeholder": "Option {number}",
"compose_form.poll.single": "Choisissez-en un", "compose_form.poll.single": "Choix unique",
"compose_form.poll.switch_to_multiple": "Changer le sondage pour autoriser plusieurs choix", "compose_form.poll.switch_to_multiple": "Changer le sondage pour autoriser plusieurs choix",
"compose_form.poll.switch_to_single": "Changer le sondage pour n'autoriser qu'un seul choix", "compose_form.poll.switch_to_single": "Changer le sondage pour n'autoriser qu'un seul choix",
"compose_form.poll.type": "Style", "compose_form.poll.type": "Style",
@ -585,9 +585,9 @@
"privacy.private.short": "Abonnés", "privacy.private.short": "Abonnés",
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
"privacy.public.short": "Public", "privacy.public.short": "Public",
"privacy.unlisted.additional": "Cette option se comporte exactement comme l'option publique, sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, l'exploration ou la recherche Mastodon, même si vous avez opté pour l'option publique pour l'ensemble de votre compte.", "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.",
"privacy.unlisted.long": "Moins de fanfares algorithmiques", "privacy.unlisted.long": "Moins de fanfares algorithmiques",
"privacy.unlisted.short": "Public calme", "privacy.unlisted.short": "Public discret",
"privacy_policy.last_updated": "Dernière mise à jour {date}", "privacy_policy.last_updated": "Dernière mise à jour {date}",
"privacy_policy.title": "Politique de confidentialité", "privacy_policy.title": "Politique de confidentialité",
"recommended": "Recommandé", "recommended": "Recommandé",

View file

@ -156,7 +156,7 @@
"compose_form.poll.duration": "Durée du sondage", "compose_form.poll.duration": "Durée du sondage",
"compose_form.poll.multiple": "Choix multiple", "compose_form.poll.multiple": "Choix multiple",
"compose_form.poll.option_placeholder": "Option {number}", "compose_form.poll.option_placeholder": "Option {number}",
"compose_form.poll.single": "Choisissez-en un", "compose_form.poll.single": "Choix unique",
"compose_form.poll.switch_to_multiple": "Changer le sondage pour autoriser plusieurs choix", "compose_form.poll.switch_to_multiple": "Changer le sondage pour autoriser plusieurs choix",
"compose_form.poll.switch_to_single": "Modifier le sondage pour autoriser qu'un seul choix", "compose_form.poll.switch_to_single": "Modifier le sondage pour autoriser qu'un seul choix",
"compose_form.poll.type": "Style", "compose_form.poll.type": "Style",
@ -585,9 +585,9 @@
"privacy.private.short": "Abonnés", "privacy.private.short": "Abonnés",
"privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon",
"privacy.public.short": "Public", "privacy.public.short": "Public",
"privacy.unlisted.additional": "Cette option se comporte exactement comme l'option publique, sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, l'exploration ou la recherche Mastodon, même si vous avez opté pour l'option publique pour l'ensemble de votre compte.", "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.",
"privacy.unlisted.long": "Moins de fanfares algorithmiques", "privacy.unlisted.long": "Moins de fanfares algorithmiques",
"privacy.unlisted.short": "Public calme", "privacy.unlisted.short": "Public discret",
"privacy_policy.last_updated": "Dernière mise à jour {date}", "privacy_policy.last_updated": "Dernière mise à jour {date}",
"privacy_policy.title": "Politique de confidentialité", "privacy_policy.title": "Politique de confidentialité",
"recommended": "Recommandé", "recommended": "Recommandé",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Este perfil foi agochado pola moderación de {domain}.", "limited_account_hint.title": "Este perfil foi agochado pola moderación de {domain}.",
"link_preview.author": "Por {name}", "link_preview.author": "Por {name}",
"link_preview.more_from_author": "Máis de {name}", "link_preview.more_from_author": "Máis de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}",
"lists.account.add": "Engadir á listaxe", "lists.account.add": "Engadir á listaxe",
"lists.account.remove": "Eliminar da listaxe", "lists.account.remove": "Eliminar da listaxe",
"lists.delete": "Eliminar listaxe", "lists.delete": "Eliminar listaxe",

View file

@ -19,7 +19,7 @@
"account.block_domain": "Blocar dominio {domain}", "account.block_domain": "Blocar dominio {domain}",
"account.block_short": "Blocar", "account.block_short": "Blocar",
"account.blocked": "Blocate", "account.blocked": "Blocate",
"account.browse_more_on_origin_server": "Percurrer plus sur le profilo original", "account.browse_more_on_origin_server": "Explorar plus sur le profilo original",
"account.cancel_follow_request": "Cancellar sequimento", "account.cancel_follow_request": "Cancellar sequimento",
"account.copy": "Copiar ligamine a profilo", "account.copy": "Copiar ligamine a profilo",
"account.direct": "Mentionar privatemente @{name}", "account.direct": "Mentionar privatemente @{name}",
@ -58,7 +58,7 @@
"account.open_original_page": "Aperir le pagina original", "account.open_original_page": "Aperir le pagina original",
"account.posts": "Messages", "account.posts": "Messages",
"account.posts_with_replies": "Messages e responsas", "account.posts_with_replies": "Messages e responsas",
"account.report": "Signalar @{name}", "account.report": "Reportar @{name}",
"account.requested": "Attendente le approbation. Clicca pro cancellar le requesta de sequer", "account.requested": "Attendente le approbation. Clicca pro cancellar le requesta de sequer",
"account.requested_follow": "{name} ha requestate de sequer te", "account.requested_follow": "{name} ha requestate de sequer te",
"account.share": "Compartir profilo de @{name}", "account.share": "Compartir profilo de @{name}",
@ -111,7 +111,7 @@
"bundle_modal_error.message": "Un error ha occurrite durante le cargamento de iste componente.", "bundle_modal_error.message": "Un error ha occurrite durante le cargamento de iste componente.",
"bundle_modal_error.retry": "Tentar novemente", "bundle_modal_error.retry": "Tentar novemente",
"closed_registrations.other_server_instructions": "Perque Mastodon es decentralisate, tu pote crear un conto sur un altere servitor e totevia interager con iste servitor.", "closed_registrations.other_server_instructions": "Perque Mastodon es decentralisate, tu pote crear un conto sur un altere servitor e totevia interager con iste servitor.",
"closed_registrations_modal.description": "Crear un conto in {domain} actualmente non es possibile, ma considera que non es necessari haber un conto specificamente sur {domain} pro usar Mastodon.", "closed_registrations_modal.description": "Crear un conto sur {domain} non es actualmente possibile, ma considera que non es necessari haber un conto specificamente sur {domain} pro usar Mastodon.",
"closed_registrations_modal.find_another_server": "Cercar un altere servitor", "closed_registrations_modal.find_another_server": "Cercar un altere servitor",
"closed_registrations_modal.preamble": "Mastodon es decentralisate, dunque, non importa ubi tu crea tu conto, tu pote sequer e communicar con omne persona sur iste servitor. Tu pote mesmo hospitar tu proprie servitor!", "closed_registrations_modal.preamble": "Mastodon es decentralisate, dunque, non importa ubi tu crea tu conto, tu pote sequer e communicar con omne persona sur iste servitor. Tu pote mesmo hospitar tu proprie servitor!",
"closed_registrations_modal.title": "Crear un conto sur Mastodon", "closed_registrations_modal.title": "Crear un conto sur Mastodon",
@ -274,7 +274,7 @@
"error.unexpected_crash.next_steps": "Tenta refrescar le pagina. Si isto non remedia le problema, es possibile que tu pote totevia usar Mastodon per medio de un altere navigator o application native.", "error.unexpected_crash.next_steps": "Tenta refrescar le pagina. Si isto non remedia le problema, es possibile que tu pote totevia usar Mastodon per medio de un altere navigator o application native.",
"error.unexpected_crash.next_steps_addons": "Tenta disactivar istes e refrescar le pagina. Si isto non remedia le problema, es possibile que tu pote totevia usar Mastodon per medio de un altere navigator o application native.", "error.unexpected_crash.next_steps_addons": "Tenta disactivar istes e refrescar le pagina. Si isto non remedia le problema, es possibile que tu pote totevia usar Mastodon per medio de un altere navigator o application native.",
"errors.unexpected_crash.copy_stacktrace": "Copiar le traciamento del pila al area de transferentia", "errors.unexpected_crash.copy_stacktrace": "Copiar le traciamento del pila al area de transferentia",
"errors.unexpected_crash.report_issue": "Signalar un defecto", "errors.unexpected_crash.report_issue": "Reportar problema",
"explore.search_results": "Resultatos de recerca", "explore.search_results": "Resultatos de recerca",
"explore.suggested_follows": "Personas", "explore.suggested_follows": "Personas",
"explore.title": "Explorar", "explore.title": "Explorar",
@ -468,7 +468,7 @@
"navigation_bar.search": "Cercar", "navigation_bar.search": "Cercar",
"navigation_bar.security": "Securitate", "navigation_bar.security": "Securitate",
"not_signed_in_indicator.not_signed_in": "Es necessari aperir session pro acceder a iste ressource.", "not_signed_in_indicator.not_signed_in": "Es necessari aperir session pro acceder a iste ressource.",
"notification.admin.report": "{name} ha signalate {target}", "notification.admin.report": "{name} ha reportate {target}",
"notification.admin.sign_up": "{name} se ha inscribite", "notification.admin.sign_up": "{name} se ha inscribite",
"notification.favourite": "{name} ha marcate tu message como favorite", "notification.favourite": "{name} ha marcate tu message como favorite",
"notification.follow": "{name} te ha sequite", "notification.follow": "{name} te ha sequite",
@ -499,7 +499,7 @@
"notification_requests.title": "Notificationes filtrate", "notification_requests.title": "Notificationes filtrate",
"notifications.clear": "Rader notificationes", "notifications.clear": "Rader notificationes",
"notifications.clear_confirmation": "Es tu secur que tu vole rader permanentemente tote tu notificationes?", "notifications.clear_confirmation": "Es tu secur que tu vole rader permanentemente tote tu notificationes?",
"notifications.column_settings.admin.report": "Nove signalationes:", "notifications.column_settings.admin.report": "Nove reportos:",
"notifications.column_settings.admin.sign_up": "Nove inscriptiones:", "notifications.column_settings.admin.sign_up": "Nove inscriptiones:",
"notifications.column_settings.alert": "Notificationes de scriptorio", "notifications.column_settings.alert": "Notificationes de scriptorio",
"notifications.column_settings.favourite": "Favorites:", "notifications.column_settings.favourite": "Favorites:",
@ -636,7 +636,7 @@
"report.close": "Facite", "report.close": "Facite",
"report.comment.title": "Ha il altere cosas que nos deberea saper?", "report.comment.title": "Ha il altere cosas que nos deberea saper?",
"report.forward": "Reinviar a {target}", "report.forward": "Reinviar a {target}",
"report.forward_hint": "Le conto es de un altere servitor. Inviar un copia anonymisate del signalation a illo tamben?", "report.forward_hint": "Le conto es de un altere servitor. Inviar un copia anonymisate del reporto a illo tamben?",
"report.mute": "Silentiar", "report.mute": "Silentiar",
"report.mute_explanation": "Tu non videra le messages de iste persona. Ille pote totevia sequer te e vider tu messages e non sapera de esser silentiate.", "report.mute_explanation": "Tu non videra le messages de iste persona. Ille pote totevia sequer te e vider tu messages e non sapera de esser silentiate.",
"report.next": "Sequente", "report.next": "Sequente",
@ -656,11 +656,11 @@
"report.statuses.subtitle": "Selige tote le responsas appropriate", "report.statuses.subtitle": "Selige tote le responsas appropriate",
"report.statuses.title": "Existe alcun messages que appoia iste reporto?", "report.statuses.title": "Existe alcun messages que appoia iste reporto?",
"report.submit": "Submitter", "report.submit": "Submitter",
"report.target": "Signalamento de {target}", "report.target": "Reportage de {target}",
"report.thanks.take_action": "Ecce tu optiones pro controlar lo que tu vide sur Mastodon:", "report.thanks.take_action": "Ecce tu optiones pro controlar lo que tu vide sur Mastodon:",
"report.thanks.take_action_actionable": "Durante que nos revide isto, tu pote prender mesuras contra @{name}:", "report.thanks.take_action_actionable": "Durante que nos revide isto, tu pote prender mesuras contra @{name}:",
"report.thanks.title": "Non vole vider isto?", "report.thanks.title": "Non vole vider isto?",
"report.thanks.title_actionable": "Gratias pro signalar, nos investigara isto.", "report.thanks.title_actionable": "Gratias pro reportar, nos investigara isto.",
"report.unfollow": "Cessar de sequer @{name}", "report.unfollow": "Cessar de sequer @{name}",
"report.unfollow_explanation": "Tu seque iste conto. Pro non plus vider su messages in tu fluxo de initio, cessa de sequer lo.", "report.unfollow_explanation": "Tu seque iste conto. Pro non plus vider su messages in tu fluxo de initio, cessa de sequer lo.",
"report_notification.attached_statuses": "{count, plural, one {{count} message} other {{count} messages}} annexate", "report_notification.attached_statuses": "{count, plural, one {{count} message} other {{count} messages}} annexate",
@ -747,7 +747,7 @@
"status.replied_to": "Respondite a {name}", "status.replied_to": "Respondite a {name}",
"status.reply": "Responder", "status.reply": "Responder",
"status.replyAll": "Responder al discussion", "status.replyAll": "Responder al discussion",
"status.report": "Signalar @{name}", "status.report": "Reportar @{name}",
"status.sensitive_warning": "Contento sensibile", "status.sensitive_warning": "Contento sensibile",
"status.share": "Compartir", "status.share": "Compartir",
"status.show_filter_reason": "Monstrar in omne caso", "status.show_filter_reason": "Monstrar in omne caso",

View file

@ -299,6 +299,11 @@
"follow_suggestions.dismiss": "Jangan tampilkan lagi", "follow_suggestions.dismiss": "Jangan tampilkan lagi",
"follow_suggestions.hints.featured": "Profil ini telah dipilih sendiri oleh tim {domain}.", "follow_suggestions.hints.featured": "Profil ini telah dipilih sendiri oleh tim {domain}.",
"follow_suggestions.hints.friends_of_friends": "Profil ini populer di kalangan orang yang anda ikuti.", "follow_suggestions.hints.friends_of_friends": "Profil ini populer di kalangan orang yang anda ikuti.",
"follow_suggestions.personalized_suggestion": "Saran yang dipersonalisasi",
"follow_suggestions.popular_suggestion": "Saran populer",
"follow_suggestions.popular_suggestion_longer": "Populer di {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Serupa dengan profil yang baru Anda ikuti",
"follow_suggestions.view_all": "Lihat semua",
"followed_tags": "Tagar yang diikuti", "followed_tags": "Tagar yang diikuti",
"footer.about": "Tentang", "footer.about": "Tentang",
"footer.directory": "Direktori profil", "footer.directory": "Direktori profil",
@ -324,6 +329,7 @@
"home.column_settings.show_reblogs": "Tampilkan boost", "home.column_settings.show_reblogs": "Tampilkan boost",
"home.column_settings.show_replies": "Tampilkan balasan", "home.column_settings.show_replies": "Tampilkan balasan",
"home.hide_announcements": "Sembunyikan pengumuman", "home.hide_announcements": "Sembunyikan pengumuman",
"home.pending_critical_update.link": "Lihat pembaruan",
"home.show_announcements": "Tampilkan pengumuman", "home.show_announcements": "Tampilkan pengumuman",
"interaction_modal.description.follow": "Dengan sebuah akun di Mastodon, Anda bisa mengikuti {name} untuk menerima kirimannya di beranda Anda.", "interaction_modal.description.follow": "Dengan sebuah akun di Mastodon, Anda bisa mengikuti {name} untuk menerima kirimannya di beranda Anda.",
"interaction_modal.description.reblog": "Dengan sebuah akun di Mastodon, Anda bisa mem-boost kiriman ini untuk membagikannya ke pengikut Anda sendiri.", "interaction_modal.description.reblog": "Dengan sebuah akun di Mastodon, Anda bisa mem-boost kiriman ini untuk membagikannya ke pengikut Anda sendiri.",
@ -375,6 +381,7 @@
"lightbox.previous": "Sebelumnya", "lightbox.previous": "Sebelumnya",
"limited_account_hint.action": "Tetap tampilkan profil", "limited_account_hint.action": "Tetap tampilkan profil",
"limited_account_hint.title": "Profil ini telah disembunyikan oleh moderator {domain}.", "limited_account_hint.title": "Profil ini telah disembunyikan oleh moderator {domain}.",
"link_preview.author": "Oleh {name}",
"lists.account.add": "Tambah ke daftar", "lists.account.add": "Tambah ke daftar",
"lists.account.remove": "Hapus dari daftar", "lists.account.remove": "Hapus dari daftar",
"lists.delete": "Hapus daftar", "lists.delete": "Hapus daftar",
@ -389,8 +396,11 @@
"lists.search": "Cari di antara orang yang Anda ikuti", "lists.search": "Cari di antara orang yang Anda ikuti",
"lists.subheading": "Daftar Anda", "lists.subheading": "Daftar Anda",
"load_pending": "{count, plural, other {# item baru}}", "load_pending": "{count, plural, other {# item baru}}",
"loading_indicator.label": "Memuat…",
"media_gallery.toggle_visible": "Tampil/Sembunyikan", "media_gallery.toggle_visible": "Tampil/Sembunyikan",
"moved_to_account_banner.text": "Akun {disabledAccount} Anda kini dinonaktifkan karena Anda pindah ke {movedToAccount}.", "moved_to_account_banner.text": "Akun {disabledAccount} Anda kini dinonaktifkan karena Anda pindah ke {movedToAccount}.",
"mute_modal.hide_options": "Sembunyikan opsi",
"mute_modal.title": "Bisukan pengguna?",
"navigation_bar.about": "Tentang", "navigation_bar.about": "Tentang",
"navigation_bar.blocks": "Pengguna diblokir", "navigation_bar.blocks": "Pengguna diblokir",
"navigation_bar.bookmarks": "Markah", "navigation_bar.bookmarks": "Markah",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Þetta notandasnið hefur verið falið af umsjónarmönnum {domain}.", "limited_account_hint.title": "Þetta notandasnið hefur verið falið af umsjónarmönnum {domain}.",
"link_preview.author": "Eftir {name}", "link_preview.author": "Eftir {name}",
"link_preview.more_from_author": "Meira frá {name}", "link_preview.more_from_author": "Meira frá {name}",
"link_preview.shares": "{count, plural, one {{counter} færsla} other {{counter} færslur}}",
"lists.account.add": "Bæta á lista", "lists.account.add": "Bæta á lista",
"lists.account.remove": "Fjarlægja af lista", "lists.account.remove": "Fjarlægja af lista",
"lists.delete": "Eyða lista", "lists.delete": "Eyða lista",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Questo profilo è stato nascosto dai moderatori di {domain}.", "limited_account_hint.title": "Questo profilo è stato nascosto dai moderatori di {domain}.",
"link_preview.author": "Di {name}", "link_preview.author": "Di {name}",
"link_preview.more_from_author": "Altro da {name}", "link_preview.more_from_author": "Altro da {name}",
"link_preview.shares": "{count, plural,one {{counter} post}other {{counter} post}}",
"lists.account.add": "Aggiungi all'elenco", "lists.account.add": "Aggiungi all'elenco",
"lists.account.remove": "Rimuovi dall'elenco", "lists.account.remove": "Rimuovi dall'elenco",
"lists.delete": "Elimina elenco", "lists.delete": "Elimina elenco",

View file

@ -414,7 +414,7 @@
"limited_account_hint.action": "그래도 프로필 보기", "limited_account_hint.action": "그래도 프로필 보기",
"limited_account_hint.title": "이 프로필은 {domain}의 중재자에 의해 숨겨진 상태입니다.", "limited_account_hint.title": "이 프로필은 {domain}의 중재자에 의해 숨겨진 상태입니다.",
"link_preview.author": "{name}", "link_preview.author": "{name}",
"link_preview.more_from_author": "{name} 더 둘러보기", "link_preview.more_from_author": "{name} 프로필 보기",
"lists.account.add": "리스트에 추가", "lists.account.add": "리스트에 추가",
"lists.account.remove": "리스트에서 제거", "lists.account.remove": "리스트에서 제거",
"lists.delete": "리스트 삭제", "lists.delete": "리스트 삭제",

View file

@ -1,7 +1,9 @@
{ {
"about.contact": "Ratio:", "about.contact": "Ratio:",
"about.domain_blocks.no_reason_available": "Ratio abdere est", "about.domain_blocks.no_reason_available": "Ratio abdere est",
"about.domain_blocks.silenced.explanation": "Tua profilia atque tuum contentum ab hac serve praecipue non videbis, nisi explōrēs expresse aut subsequeris et optēs.",
"account.account_note_header": "Annotatio", "account.account_note_header": "Annotatio",
"account.add_or_remove_from_list": "Adde aut ēripe ex tabellīs",
"account.badges.bot": "Robotum", "account.badges.bot": "Robotum",
"account.badges.group": "Congregatio", "account.badges.group": "Congregatio",
"account.block": "Impedire @{name}", "account.block": "Impedire @{name}",
@ -11,11 +13,21 @@
"account.domain_blocked": "Dominium impeditum", "account.domain_blocked": "Dominium impeditum",
"account.edit_profile": "Recolere notionem", "account.edit_profile": "Recolere notionem",
"account.featured_tags.last_status_never": "Nulla contributa", "account.featured_tags.last_status_never": "Nulla contributa",
"account.featured_tags.title": "Hashtag notātī {name}",
"account.followers_counter": "{count, plural, one {{counter} Sectator} other {{counter} Sectatores}}",
"account.following_counter": "{count, plural, one {{counter} Sequens} other {{counter} Sequentes}}",
"account.moved_to": "{name} significavit eum suam rationem novam nunc esse:",
"account.muted": "Confutatus", "account.muted": "Confutatus",
"account.requested_follow": "{name} postulavit ut te sequeretur",
"account.statuses_counter": "{count, plural, one {{counter} Nuntius} other {{counter} Nuntii}}",
"account.unblock_short": "Solvere impedimentum", "account.unblock_short": "Solvere impedimentum",
"account_note.placeholder": "Click to add a note", "account_note.placeholder": "Click to add a note",
"admin.dashboard.retention.average": "Mediocritas", "admin.dashboard.retention.average": "Mediocritas",
"admin.impact_report.instance_accounts": "Rationes perfiles hoc deleret",
"alert.unexpected.message": "Error inopinatus occurrit.",
"announcement.announcement": "Proclamatio", "announcement.announcement": "Proclamatio",
"attachments_list.unprocessed": "(immūtātus)",
"block_modal.you_wont_see_mentions": "Nuntios quibus eos commemorant non videbis.",
"bundle_column_error.error.title": "Eheu!", "bundle_column_error.error.title": "Eheu!",
"bundle_column_error.retry": "Retemptare", "bundle_column_error.retry": "Retemptare",
"bundle_column_error.routing.title": "CCCCIIII", "bundle_column_error.routing.title": "CCCCIIII",
@ -32,30 +44,60 @@
"compose_form.direct_message_warning_learn_more": "Discere plura", "compose_form.direct_message_warning_learn_more": "Discere plura",
"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": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
"compose_form.lock_disclaimer": "Tua ratio non est {clausa}. Quisquis te sequi potest ut visum accipiat nuntios tuos tantum pro sectatoribus.",
"compose_form.lock_disclaimer.lock": "clausum", "compose_form.lock_disclaimer.lock": "clausum",
"compose_form.placeholder": "What is on your mind?", "compose_form.placeholder": "What is on your mind?",
"compose_form.publish_form": "Barrire", "compose_form.publish_form": "Barrire",
"compose_form.spoiler.marked": "Text is hidden behind warning", "compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden", "compose_form.spoiler.unmarked": "Adde praeconium contentūs",
"confirmations.block.confirm": "Impedire", "confirmations.block.confirm": "Impedire",
"confirmations.delete.confirm": "Oblitterare", "confirmations.delete.confirm": "Oblitterare",
"confirmations.delete.message": "Are you sure you want to delete this status?", "confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.delete_list.confirm": "Oblitterare", "confirmations.delete_list.confirm": "Oblitterare",
"confirmations.discard_edit_media.message": "Habēs mutationēs in descriptionem vel prōspectum medii quae nōn sunt servātae; eas dēmittam?",
"confirmations.mute.confirm": "Confutare", "confirmations.mute.confirm": "Confutare",
"confirmations.reply.confirm": "Respondere", "confirmations.reply.confirm": "Respondere",
"disabled_account_banner.account_settings": "Praeferentiae ratiōnis",
"disabled_account_banner.text": "Ratio tua {disabledAccount} debilitata est.",
"dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.", "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.",
"dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.", "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.",
"domain_block_modal.you_will_lose_followers": "Omnes sectatores tuī ex hoc servō removēbuntur.",
"domain_block_modal.you_wont_see_posts": "Nuntios aut notificātiōnēs ab usoribus in hōc servō nōn vidēbis.",
"domain_pill.activitypub_like_language": "ActivityPub est velut lingua quam Mastodon cum aliīs sociālibus rētibus loquitur.",
"domain_pill.your_handle": "Tuus nominulus:",
"domain_pill.your_server": "Tua domus digitalis, ubi omnia tua nuntia habitant. Hanc non amas? Servēs trānsferāre potes quōcumque tempore et sectātōrēs tuōs simul addūcere.",
"domain_pill.your_username": "Tuō singulāre id indicium in hōc servō est. Est possibile invenīre usōrēs cum eōdem nōmine in servīs aliīs.",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Embed this status on your website by copying the code below.",
"emoji_button.activity": "Actiō",
"emoji_button.food": "Cibus et potus", "emoji_button.food": "Cibus et potus",
"emoji_button.people": "Homines", "emoji_button.people": "Homines",
"emoji_button.search": "Quaerere...", "emoji_button.search": "Quaerere...",
"empty_column.account_suspended": "Rātiō suspēnsa",
"empty_column.account_timeline": "Hic nulla contributa!", "empty_column.account_timeline": "Hic nulla contributa!",
"empty_column.account_unavailable": "Notio non impetrabilis", "empty_column.account_unavailable": "Notio non impetrabilis",
"empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}", "empty_column.blocks": "Nondum quemquam usorem obsēcāvisti.",
"empty_column.direct": "Nōn habēs adhūc ullo mentionēs prīvātās. Cum ūnam mīseris aut accipis, hīc apparēbit.",
"empty_column.followed_tags": "Nōn adhūc aliquem hastāginem secūtus es. Cum id fēceris, hic ostendētur.",
"empty_column.home": "Tua linea temporum domesticus vacua est! Sequere plures personas ut eam compleas.",
"empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.",
"empty_column.lists": "Nōn adhūc habēs ullo tabellās. Cum creās, hīc apparēbunt.",
"empty_column.mutes": "Nondum quemquam usorem tacuisti.",
"empty_column.notification_requests": "Omnia clara sunt! Nihil hic est. Cum novās notificātiōnēs accipīs, hic secundum tua praecepta apparebunt.",
"empty_column.notifications": "Nōn adhūc habēs ullo notificātiōnēs. Cum aliī tē interagunt, hīc videbis.",
"explore.trending_statuses": "Contributa", "explore.trending_statuses": "Contributa",
"filtered_notifications_banner.mentions": "{count, plural, one {mentiō} other {mentiōnēs}}",
"firehose.all": "Omnis",
"footer.about": "De",
"generic.saved": "Servavit", "generic.saved": "Servavit",
"hashtag.column_settings.tag_mode.all": "Haec omnia",
"hashtag.column_settings.tag_toggle": "Include additional tags in this column", "hashtag.column_settings.tag_toggle": "Include additional tags in this column",
"hashtag.counter_by_accounts": "{count, plural, one {{counter} particeps} other {{counter} participēs}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} nuntius} other {{counter} nuntii}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} nuntius} other {{counter} nuntii}} hodie",
"hashtags.and_other": "…et {count, plural, other {# plus}}",
"intervals.full.days": "{number, plural, one {# die} other {# dies}}",
"intervals.full.hours": "{number, plural, one {# hora} other {# horae}}",
"intervals.full.minutes": "{number, plural, one {# minutum} other {# minuta}}",
"keyboard_shortcuts.back": "Re navigare", "keyboard_shortcuts.back": "Re navigare",
"keyboard_shortcuts.blocked": "Aperire listam usorum obstructorum", "keyboard_shortcuts.blocked": "Aperire listam usorum obstructorum",
"keyboard_shortcuts.boost": "Inlustrare publicatio", "keyboard_shortcuts.boost": "Inlustrare publicatio",
@ -89,17 +131,47 @@
"keyboard_shortcuts.up": "to move up in the list", "keyboard_shortcuts.up": "to move up in the list",
"lightbox.close": "Claudere", "lightbox.close": "Claudere",
"lightbox.next": "Secundum", "lightbox.next": "Secundum",
"lists.account.add": "Adde ad tabellās",
"lists.new.create": "Addere tabella",
"load_pending": "{count, plural, one {# novum item} other {# nova itema}}",
"media_gallery.toggle_visible": "{number, plural, one {Cēla imaginem} other {Cēla imagines}}",
"moved_to_account_banner.text": "Tua ratione {disabledAccount} interdum reposita est, quod ad {movedToAccount} migrāvisti.",
"mute_modal.you_wont_see_mentions": "Non videbis nuntios quī eōs commemorant.",
"navigation_bar.about": "De",
"navigation_bar.domain_blocks": "Hidden domains", "navigation_bar.domain_blocks": "Hidden domains",
"not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.", "not_signed_in_indicator.not_signed_in": "Ad hunc locum pervenire oportet ut inīre facias.",
"notification.reblog": "{name} boosted your status", "notification.admin.report": "{name} nuntiavit {target}",
"notification.admin.sign_up": "{name} subscripsit",
"notification.favourite": "{name} nuntium tuum favit",
"notification.follow": "{name} te secutus est",
"notification.follow_request": "{name} postulavit ut te sequeretur",
"notification.mention": "{name} memoravi",
"notification.moderation_warning": "Accepistī monitionem moderationis.",
"notification.moderation_warning.action_disable": "Ratio tua debilitata est.",
"notification.moderation_warning.action_none": "Tua ratiō monitum moderātiōnis accēpit.",
"notification.moderation_warning.action_sensitive": "Tua nuntia hinc sensibiliter notabuntur.",
"notification.moderation_warning.action_silence": "Ratio tua est limitata.",
"notification.moderation_warning.action_suspend": "Ratio tua suspensus est.",
"notification.own_poll": "Suffragium tuum terminatum est.",
"notification.poll": "Electione in quam suffragium dedisti finita est.",
"notification.reblog": "{name} tuum nuntium amplificavit.",
"notification.relationships_severance_event.account_suspension": "Admin ab {from} {target} suspendit, quod significat nōn iam posse tē novitātēs ab eīs accipere aut cum eīs interagere.",
"notification.relationships_severance_event.domain_block": "Admin ab {from} {target} obsēcāvit, includēns {followersCount} ex tuīs sectātōribus et {followingCount, plural, one {# ratione} other {# rationibus}} quās sequeris.",
"notification.relationships_severance_event.user_domain_block": "Bloqueāstī {target}, removēns {followersCount} ex sectātōribus tuīs et {followingCount, plural, one {# rationem} other {# rationēs}} quōs sequeris.",
"notification.status": "{name} nuper publicavit",
"notification.update": "{name} nuntium correxit",
"notification_requests.accept": "Accipe",
"notifications.filter.all": "Omnia", "notifications.filter.all": "Omnia",
"notifications.filter.polls": "Eventus electionis", "notifications.filter.polls": "Eventus electionis",
"notifications.group": "Notificātiōnēs",
"onboarding.actions.go_to_explore": "See what's trending", "onboarding.actions.go_to_explore": "See what's trending",
"onboarding.actions.go_to_home": "Go to your home feed", "onboarding.actions.go_to_home": "Go to your home feed",
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", "onboarding.follows.lead": "Tua domus feed est principalis via Mastodon experīrī. Quō plūrēs persōnas sequeris, eō actīvior et interessantior erit. Ad tē incipiendum, ecce quaedam suāsiones:",
"onboarding.follows.title": "Popular on Mastodon", "onboarding.follows.title": "Popular on Mastodon",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.profile.display_name_hint": "Tuum nomen completum aut tuum nomen ludens...",
"onboarding.start.lead": "Nunc pars es Mastodonis, singularis, socialis medii platformae decentralis ubi—non algorismus—tuam ipsius experientiam curas. Incipiāmus in nova hac socialis regione:",
"onboarding.start.skip": "Want to skip right ahead?", "onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "Perfecisti eam!",
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.", "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}", "onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
"onboarding.steps.publish_status.body": "Say hello to the world.", "onboarding.steps.publish_status.body": "Say hello to the world.",
@ -107,29 +179,48 @@
"onboarding.steps.setup_profile.title": "Customize your profile", "onboarding.steps.setup_profile.title": "Customize your profile",
"onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!", "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!",
"onboarding.steps.share_profile.title": "Share your profile", "onboarding.steps.share_profile.title": "Share your profile",
"onboarding.tips.2fa": "<strong>Scisne?</strong> Tūam ratiōnem sēcūrāre potes duōrum elementōrum authentīcātiōnem in ratiōnis tuī praeferentiīs statuendō. Cum ūllā app TOTP ex tuā ēlēctiōne operātur, numerus tēlephōnicus necessārius nōn est!",
"onboarding.tips.accounts_from_other_servers": "<strong>Scisne?</strong> Quoniam Mastodon dēcentrālis est, nōnnulla profīlia quae invenīs in servīs aliīs quam tuōrum erunt hospitāta. Tamen cum eīs sine impedīmentō interāgere potes! Servus eōrum in alterā parte nōminis eōrum est!",
"onboarding.tips.migration": "<strong>Scisne?</strong> Sī sentīs {domain} tibi in futūrō nōn esse optimam servī ēlēctiōnem, ad alium servum Mastodon sine amittendō sectātōribus tuīs migrāre potes. Etiam tuum servum hospitārī potes!",
"onboarding.tips.verification": "<strong>Scisne?</strong> Tūam ratiōnem verificāre potes iungendō nexum ad prōfīlium Mastodon tuum in propriā pāginā interrētiā et addendō pāginam ad prōfīlium tuum. Nullae pecūniae aut documenta necessāria sunt!",
"poll.closed": "Clausum", "poll.closed": "Clausum",
"poll.total_people": "{count, plural, one {# persona} other {# personae}}",
"poll.total_votes": "{count, plural, one {# suffragium} other {# suffragia}}",
"poll.vote": "Eligere", "poll.vote": "Eligere",
"poll.voted": "Elegisti hoc responsum", "poll.voted": "Elegisti hoc responsum",
"poll.votes": "{votes, plural, one {# sufragium} other {# sufragia}}",
"poll_button.add_poll": "Addere electionem", "poll_button.add_poll": "Addere electionem",
"poll_button.remove_poll": "Auferre electionem", "poll_button.remove_poll": "Auferre electionem",
"privacy.change": "Adjust status privacy", "privacy.change": "Adjust status privacy",
"privacy.public.short": "Coram publico", "privacy.public.short": "Coram publico",
"regeneration_indicator.sublabel": "Tua domus feed praeparātur!",
"relative_time.full.days": "{number, plural, one {# ante die} other {# ante dies}}",
"relative_time.full.hours": "{number, plural, one {# ante horam} other {# ante horas}}",
"relative_time.full.just_now": "nunc", "relative_time.full.just_now": "nunc",
"relative_time.full.minutes": "{number, plural, one {# ante minutum} other {# ante minuta}}",
"relative_time.full.seconds": "{number, plural, one {# ante secundum} other {# ante secunda}}",
"relative_time.just_now": "nunc", "relative_time.just_now": "nunc",
"relative_time.today": "hodie", "relative_time.today": "hodie",
"reply_indicator.attachments": "{count, plural, one {# annexus} other {# annexūs}}",
"report.block": "Impedimentum", "report.block": "Impedimentum",
"report.block_explanation": "Non videbis eorum nuntios. Non poterunt vidēre tuōs nuntios aut tē sequī. Intelligere poterunt sē obstrūctōs esse.",
"report.categories.other": "Altera", "report.categories.other": "Altera",
"report.category.title_account": "notio", "report.category.title_account": "notio",
"report.category.title_status": "contributum", "report.category.title_status": "contributum",
"report.close": "Confectum", "report.close": "Confectum",
"report.mute": "Confutare", "report.mute": "Confutare",
"report.mute_explanation": "Non videbis eōrum nuntiōs. Possunt adhuc tē sequī et tuōs nuntiōs vidēre, nec sciēbunt sē tacitōs esse.",
"report.next": "Secundum", "report.next": "Secundum",
"report.placeholder": "Type or paste additional comments", "report.placeholder": "Commentāriī adiūnctī",
"report.submit": "Mittere", "report.submit": "Mittere",
"report.target": "Report {target}", "report.target": "Report {target}",
"report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached", "report_notification.attached_statuses": "{count, plural, one {{count} nuntius} other {{count} nuntii}} attachiatus",
"report_notification.categories.other": "Altera", "report_notification.categories.other": "Altera",
"search.placeholder": "Quaerere", "search.placeholder": "Quaerere",
"search_results.all": "Omnis",
"server_banner.active_users": "Usūrāriī āctīvī",
"server_banner.administered_by": "Administratur:",
"server_banner.introduction": "{domain} pars est de rete sociali decentralizato a {mastodon} propulsato.",
"server_banner.learn_more": "Discere plura", "server_banner.learn_more": "Discere plura",
"sign_in_banner.sign_in": "Sign in", "sign_in_banner.sign_in": "Sign in",
"status.admin_status": "Open this status in the moderation interface", "status.admin_status": "Open this status in the moderation interface",
@ -139,13 +230,29 @@
"status.delete": "Oblitterare", "status.delete": "Oblitterare",
"status.edit": "Recolere", "status.edit": "Recolere",
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
"status.favourites": "{count, plural, one {favoritum} other {favorita}}",
"status.history.created": "{name} creatum {date}",
"status.history.edited": "{name} correxit {date}",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", "status.reblogged_by": "{name} adiuvavit",
"status.reblogs": "{count, plural, one {auctus} other {auctūs}}",
"status.title.with_attachments": "{user} publicavit {attachmentCount, plural, one {unum annexum} other {{attachmentCount} annexa}}",
"tabs_bar.home": "Domi", "tabs_bar.home": "Domi",
"time_remaining.days": "{number, plural, one {# die} other {# dies}} restant",
"time_remaining.hours": "{number, plural, one {# hora} other {# horae}} restant",
"time_remaining.minutes": "{number, plural, one {# minutum} other {# minuta}} restant",
"time_remaining.seconds": "{number, plural, one {# secundum} other {# secunda}} restant",
"timeline_hint.remote_resource_not_displayed": "{resource} ab aliīs servīs nōn ostenduntur.",
"timeline_hint.resources.statuses": "Contributa pristina", "timeline_hint.resources.statuses": "Contributa pristina",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}", "trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {diē prīdiē} other {diēbus praeteritīs {days}}}",
"ui.beforeunload": "Si Mastodon discesseris, tua epitome peribit.",
"units.short.billion": "{count} millia milionum",
"units.short.million": "{count} milionum",
"units.short.thousand": "{count} millia",
"upload_button.label": "Imaginēs, vīdeō aut fīle audītūs adde",
"upload_form.audio_description": "Describe for people who are hard of hearing", "upload_form.audio_description": "Describe for people who are hard of hearing",
"upload_form.edit": "Recolere", "upload_form.edit": "Recolere",
"upload_modal.description_placeholder": "A velox brunneis vulpes salit super piger canis",
"upload_progress.label": "Uploading…", "upload_progress.label": "Uploading…",
"video.mute": "Confutare soni" "video.mute": "Confutare soni"
} }

View file

@ -217,7 +217,7 @@
"domain_block_modal.title": "Blokuoti domeną?", "domain_block_modal.title": "Blokuoti domeną?",
"domain_block_modal.you_will_lose_followers": "Visi tavo sekėjai iš šio serverio bus pašalinti.", "domain_block_modal.you_will_lose_followers": "Visi tavo sekėjai iš šio serverio bus pašalinti.",
"domain_block_modal.you_wont_see_posts": "Nematysi naudotojų įrašų ar pranešimų šiame serveryje.", "domain_block_modal.you_wont_see_posts": "Nematysi naudotojų įrašų ar pranešimų šiame serveryje.",
"domain_pill.activitypub_lets_connect": "Tai leidžia tau sąveikauti su žmonėmis ne tik Mastodon, bet ir įvairiose socialinėse programėlėse.", "domain_pill.activitypub_lets_connect": "Tai leidžia tau prisijungti ir bendrauti su žmonėmis ne tik Mastodon, bet ir įvairiose socialinėse programėlėse.",
"domain_pill.activitypub_like_language": "ActivityPub tai tarsi kalba, kuria Mastodon kalba su kitais socialiniais tinklais.", "domain_pill.activitypub_like_language": "ActivityPub tai tarsi kalba, kuria Mastodon kalba su kitais socialiniais tinklais.",
"domain_pill.server": "Serveris", "domain_pill.server": "Serveris",
"domain_pill.their_handle": "Jų socialinis medijos vardas:", "domain_pill.their_handle": "Jų socialinis medijos vardas:",
@ -433,7 +433,15 @@
"loading_indicator.label": "Kraunama…", "loading_indicator.label": "Kraunama…",
"media_gallery.toggle_visible": "{number, plural, one {Slėpti vaizdą} few {Slėpti vaizdus} many {Slėpti vaizdo} other {Slėpti vaizdų}}", "media_gallery.toggle_visible": "{number, plural, one {Slėpti vaizdą} few {Slėpti vaizdus} many {Slėpti vaizdo} other {Slėpti vaizdų}}",
"moved_to_account_banner.text": "Tavo paskyra {disabledAccount} šiuo metu išjungta, nes persikėlei į {movedToAccount}.", "moved_to_account_banner.text": "Tavo paskyra {disabledAccount} šiuo metu išjungta, nes persikėlei į {movedToAccount}.",
"mute_modal.hide_from_notifications": "Slėpti nuo pranešimų",
"mute_modal.hide_options": "Slėpti parinktis",
"mute_modal.indefinite": "Kol atšauksiu jų nutildymą",
"mute_modal.show_options": "Rodyti parinktis", "mute_modal.show_options": "Rodyti parinktis",
"mute_modal.they_can_mention_and_follow": "Jie gali tave paminėti ir sekti, bet tu jų nematysi.",
"mute_modal.they_wont_know": "Jie nežinos, kad buvo nutildyti.",
"mute_modal.title": "Nutildyti naudotoją?",
"mute_modal.you_wont_see_mentions": "Nematysi įrašus, kuriuose jie paminimi.",
"mute_modal.you_wont_see_posts": "Jie vis tiek gali matyti tavo įrašus, bet tu nematysi jų.",
"navigation_bar.about": "Apie", "navigation_bar.about": "Apie",
"navigation_bar.advanced_interface": "Atidaryti išplėstinę žiniatinklio sąsają", "navigation_bar.advanced_interface": "Atidaryti išplėstinę žiniatinklio sąsają",
"navigation_bar.blocks": "Užblokuoti naudotojai", "navigation_bar.blocks": "Užblokuoti naudotojai",
@ -478,6 +486,7 @@
"notification.own_poll": "Tavo apklausa baigėsi", "notification.own_poll": "Tavo apklausa baigėsi",
"notification.poll": "Apklausa, kurioje balsavai, pasibaigė", "notification.poll": "Apklausa, kurioje balsavai, pasibaigė",
"notification.reblog": "{name} pakėlė tavo įrašą", "notification.reblog": "{name} pakėlė tavo įrašą",
"notification.relationships_severance_event": "Prarasti sąryšiai su {name}",
"notification.relationships_severance_event.learn_more": "Sužinoti daugiau", "notification.relationships_severance_event.learn_more": "Sužinoti daugiau",
"notification.relationships_severance_event.user_domain_block": "Tu užblokavai {target}. Pašalinama {followersCount} savo sekėjų ir {followingCount, plural, one {# paskyrą} few {# paskyrai} many {# paskyros} other {# paskyrų}}, kurios seki.", "notification.relationships_severance_event.user_domain_block": "Tu užblokavai {target}. Pašalinama {followersCount} savo sekėjų ir {followingCount, plural, one {# paskyrą} few {# paskyrai} many {# paskyros} other {# paskyrų}}, kurios seki.",
"notification.status": "{name} ką tik paskelbė", "notification.status": "{name} ką tik paskelbė",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Dit profiel is door de moderatoren van {domain} verborgen.", "limited_account_hint.title": "Dit profiel is door de moderatoren van {domain} verborgen.",
"link_preview.author": "Door {name}", "link_preview.author": "Door {name}",
"link_preview.more_from_author": "Meer van {name}", "link_preview.more_from_author": "Meer van {name}",
"link_preview.shares": "{count, plural, one {{counter} bericht} other {{counter} berichten}}",
"lists.account.add": "Aan lijst toevoegen", "lists.account.add": "Aan lijst toevoegen",
"lists.account.remove": "Uit lijst verwijderen", "lists.account.remove": "Uit lijst verwijderen",
"lists.delete": "Lijst verwijderen", "lists.delete": "Lijst verwijderen",

View file

@ -414,6 +414,8 @@
"limited_account_hint.action": "Vis profilen likevel", "limited_account_hint.action": "Vis profilen likevel",
"limited_account_hint.title": "Denne profilen er skjult av moderatorane på {domain}.", "limited_account_hint.title": "Denne profilen er skjult av moderatorane på {domain}.",
"link_preview.author": "Av {name}", "link_preview.author": "Av {name}",
"link_preview.more_from_author": "Meir frå {name}",
"link_preview.shares": "{count, plural,one {{counter} innlegg} other {{counter} innlegg}}",
"lists.account.add": "Legg til i liste", "lists.account.add": "Legg til i liste",
"lists.account.remove": "Fjern frå liste", "lists.account.remove": "Fjern frå liste",
"lists.delete": "Slett liste", "lists.delete": "Slett liste",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Ten profil został ukryty przez moderatorów {domain}.", "limited_account_hint.title": "Ten profil został ukryty przez moderatorów {domain}.",
"link_preview.author": "{name}", "link_preview.author": "{name}",
"link_preview.more_from_author": "Więcej od {name}", "link_preview.more_from_author": "Więcej od {name}",
"link_preview.shares": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}",
"lists.account.add": "Dodaj do listy", "lists.account.add": "Dodaj do listy",
"lists.account.remove": "Usunąć z listy", "lists.account.remove": "Usunąć z listy",
"lists.delete": "Usuń listę", "lists.delete": "Usuń listę",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Este perfil foi ocultado pelos moderadores de {domain}.", "limited_account_hint.title": "Este perfil foi ocultado pelos moderadores de {domain}.",
"link_preview.author": "Por {name}", "link_preview.author": "Por {name}",
"link_preview.more_from_author": "Mais de {name}", "link_preview.more_from_author": "Mais de {name}",
"link_preview.shares": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
"lists.account.add": "Adicionar à lista", "lists.account.add": "Adicionar à lista",
"lists.account.remove": "Remover da lista", "lists.account.remove": "Remover da lista",
"lists.delete": "Eliminar lista", "lists.delete": "Eliminar lista",

View file

@ -391,6 +391,7 @@
"limited_account_hint.action": "Aj tak zobraziť profil", "limited_account_hint.action": "Aj tak zobraziť profil",
"limited_account_hint.title": "Tento profil bol skrytý správcami servera {domain}.", "limited_account_hint.title": "Tento profil bol skrytý správcami servera {domain}.",
"link_preview.author": "Autor: {name}", "link_preview.author": "Autor: {name}",
"link_preview.more_from_author": "Viac od {name}",
"lists.account.add": "Pridať do zoznamu", "lists.account.add": "Pridať do zoznamu",
"lists.account.remove": "Odstrániť zo zoznamu", "lists.account.remove": "Odstrániť zo zoznamu",
"lists.delete": "Vymazať zoznam", "lists.delete": "Vymazať zoznam",
@ -411,6 +412,7 @@
"moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálne deaktivovaný, pretože ste sa presunuli na {movedToAccount}.", "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálne deaktivovaný, pretože ste sa presunuli na {movedToAccount}.",
"mute_modal.hide_from_notifications": "Ukryť z upozornení", "mute_modal.hide_from_notifications": "Ukryť z upozornení",
"mute_modal.hide_options": "Skryť možnosti", "mute_modal.hide_options": "Skryť možnosti",
"mute_modal.indefinite": "Pokiaľ ich neodtíšim",
"mute_modal.show_options": "Zobraziť možnosti", "mute_modal.show_options": "Zobraziť možnosti",
"mute_modal.title": "Stíšiť užívateľa?", "mute_modal.title": "Stíšiť užívateľa?",
"navigation_bar.about": "O tomto serveri", "navigation_bar.about": "O tomto serveri",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Profil so moderatorji strežnika {domain} skrili.", "limited_account_hint.title": "Profil so moderatorji strežnika {domain} skrili.",
"link_preview.author": "Avtor_ica {name}", "link_preview.author": "Avtor_ica {name}",
"link_preview.more_from_author": "Več od {name}", "link_preview.more_from_author": "Več od {name}",
"link_preview.shares": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}",
"lists.account.add": "Dodaj na seznam", "lists.account.add": "Dodaj na seznam",
"lists.account.remove": "Odstrani s seznama", "lists.account.remove": "Odstrani s seznama",
"lists.delete": "Izbriši seznam", "lists.delete": "Izbriši seznam",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "Denna profil har dolts av {domain}s moderatorer.", "limited_account_hint.title": "Denna profil har dolts av {domain}s moderatorer.",
"link_preview.author": "Av {name}", "link_preview.author": "Av {name}",
"link_preview.more_from_author": "Mer från {name}", "link_preview.more_from_author": "Mer från {name}",
"link_preview.shares": "{count, plural, one {{counter} inlägg} other {{counter} inlägg}}",
"lists.account.add": "Lägg till i lista", "lists.account.add": "Lägg till i lista",
"lists.account.remove": "Ta bort från lista", "lists.account.remove": "Ta bort från lista",
"lists.delete": "Radera lista", "lists.delete": "Radera lista",

View file

@ -26,8 +26,9 @@
"account.featured_tags.last_status_never": "کوئی مراسلہ نہیں", "account.featured_tags.last_status_never": "کوئی مراسلہ نہیں",
"account.featured_tags.title": "{name} کے نمایاں ہیش ٹیگز", "account.featured_tags.title": "{name} کے نمایاں ہیش ٹیگز",
"account.follow": "پیروی کریں", "account.follow": "پیروی کریں",
"account.follow_back": "اکاؤنٹ کو فالو بیک ",
"account.followers": "پیروکار", "account.followers": "پیروکار",
"account.followers.empty": "\"ہنوز اس صارف کی کوئی پیروی نہیں کرتا\".", "account.followers.empty": "ہنوز اس صارف کی کوئی پیروی نہیں کرتا.",
"account.followers_counter": "{count, plural,one {{counter} پیروکار} other {{counter} پیروکار}}", "account.followers_counter": "{count, plural,one {{counter} پیروکار} other {{counter} پیروکار}}",
"account.following": "فالو کر رہے ہیں", "account.following": "فالو کر رہے ہیں",
"account.following_counter": "{count, plural, one {{counter} پیروی کر رہے ہیں} other {{counter} پیروی کر رہے ہیں}}", "account.following_counter": "{count, plural, one {{counter} پیروی کر رہے ہیں} other {{counter} پیروی کر رہے ہیں}}",
@ -46,6 +47,7 @@
"account.mute_notifications_short": "نوٹیفیکیشنز کو خاموش کریں", "account.mute_notifications_short": "نوٹیفیکیشنز کو خاموش کریں",
"account.mute_short": "خاموش", "account.mute_short": "خاموش",
"account.muted": "خاموش کردہ", "account.muted": "خاموش کردہ",
"account.mutual": "میوچول اکاؤنٹ",
"account.no_bio": "کوئی تفصیل نہیں دی گئی۔", "account.no_bio": "کوئی تفصیل نہیں دی گئی۔",
"account.open_original_page": "اصل صفحہ کھولیں", "account.open_original_page": "اصل صفحہ کھولیں",
"account.posts": "ٹوٹ", "account.posts": "ٹوٹ",
@ -64,7 +66,8 @@
"account.unmute": "@{name} کو با آواز کریں", "account.unmute": "@{name} کو با آواز کریں",
"account.unmute_notifications_short": "نوٹیفیکیشنز کو خاموش نہ کریں", "account.unmute_notifications_short": "نوٹیفیکیشنز کو خاموش نہ کریں",
"account.unmute_short": "کو خاموش نہ کریں", "account.unmute_short": "کو خاموش نہ کریں",
"account_note.placeholder": "Click to add a note", "admin.dashboard.daily_retention": "ایڈمن ڈیش بورڈ کو ڈیلی چیک ان کریں",
"admin.dashboard.monthly_retention": "ایڈمن کیش بورڈ کو منتھلی چیک ان کریں",
"admin.dashboard.retention.average": "اوسط", "admin.dashboard.retention.average": "اوسط",
"admin.dashboard.retention.cohort_size": "نئے یسرز", "admin.dashboard.retention.cohort_size": "نئے یسرز",
"alert.rate_limited.message": "\"{retry_time, time, medium} کے بعد کوشش کریں\".", "alert.rate_limited.message": "\"{retry_time, time, medium} کے بعد کوشش کریں\".",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "此账号资料已被 {domain} 管理员隐藏。", "limited_account_hint.title": "此账号资料已被 {domain} 管理员隐藏。",
"link_preview.author": "由 {name}", "link_preview.author": "由 {name}",
"link_preview.more_from_author": "查看 {name} 的更多内容", "link_preview.more_from_author": "查看 {name} 的更多内容",
"link_preview.shares": "{count, plural, other {{counter} 条嘟文}}",
"lists.account.add": "添加到列表", "lists.account.add": "添加到列表",
"lists.account.remove": "从列表中移除", "lists.account.remove": "从列表中移除",
"lists.delete": "删除列表", "lists.delete": "删除列表",

View file

@ -415,6 +415,7 @@
"limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。", "limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。",
"link_preview.author": "來自 {name}", "link_preview.author": "來自 {name}",
"link_preview.more_from_author": "來自 {name} 之更多內容", "link_preview.more_from_author": "來自 {name} 之更多內容",
"link_preview.shares": "{count, plural, other {{count} 則嘟文}}",
"lists.account.add": "新增至列表", "lists.account.add": "新增至列表",
"lists.account.remove": "自列表中移除", "lists.account.remove": "自列表中移除",
"lists.delete": "刪除列表", "lists.delete": "刪除列表",

View file

@ -903,9 +903,15 @@ body > [data-popper-placement] {
padding: 10px; padding: 10px;
p { p {
font-size: 15px;
line-height: 22px;
color: $darker-text-color; color: $darker-text-color;
margin-bottom: 20px; margin-bottom: 20px;
strong {
font-weight: 700;
}
a { a {
color: $secondary-text-color; color: $secondary-text-color;
text-decoration: none; text-decoration: none;
@ -1411,10 +1417,15 @@ body > [data-popper-placement] {
.audio-player, .audio-player,
.attachment-list, .attachment-list,
.picture-in-picture-placeholder, .picture-in-picture-placeholder,
.more-from-author,
.status-card, .status-card,
.hashtag-bar { .hashtag-bar {
margin-inline-start: $thread-margin; margin-inline-start: $thread-margin;
width: calc(100% - ($thread-margin)); width: calc(100% - $thread-margin);
}
.more-from-author {
width: calc(100% - $thread-margin + 2px);
} }
.status__content__read-more-button { .status__content__read-more-button {
@ -4129,6 +4140,13 @@ a.status-card {
border-end-start-radius: 0; border-end-start-radius: 0;
} }
.status-card.bottomless .status-card__image,
.status-card.bottomless .status-card__image-image,
.status-card.bottomless .status-card__image-preview {
border-end-end-radius: 0;
border-end-start-radius: 0;
}
.status-card.expanded > a { .status-card.expanded > a {
width: 100%; width: 100%;
} }
@ -8784,43 +8802,80 @@ noscript {
display: flex; display: flex;
align-items: center; align-items: center;
color: $primary-text-color; color: $primary-text-color;
text-decoration: none; padding: 16px;
padding: 15px;
border-bottom: 1px solid var(--background-border-color); border-bottom: 1px solid var(--background-border-color);
gap: 15px; gap: 16px;
&:last-child { &:last-child {
border-bottom: 0; border-bottom: 0;
} }
&:hover,
&:active,
&:focus {
color: $highlight-text-color;
.story__details__publisher,
.story__details__shared {
color: $highlight-text-color;
}
}
&__details { &__details {
flex: 1 1 auto; flex: 1 1 auto;
&__publisher { &__publisher {
color: $darker-text-color; color: $darker-text-color;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 14px;
line-height: 20px;
} }
&__title { &__title {
display: block;
font-size: 19px; font-size: 19px;
line-height: 24px; line-height: 24px;
font-weight: 500; font-weight: 500;
margin-bottom: 8px; margin-bottom: 8px;
text-decoration: none;
color: $primary-text-color;
&:hover,
&:active,
&:focus {
color: $highlight-text-color;
}
} }
&__shared { &__shared {
display: flex;
align-items: center;
color: $darker-text-color; color: $darker-text-color;
gap: 8px;
justify-content: space-between;
font-size: 14px;
line-height: 20px;
& > span {
display: flex;
align-items: center;
gap: 4px;
}
&__pill {
background: var(--surface-variant-background-color);
border-radius: 4px;
color: inherit;
text-decoration: none;
padding: 4px 12px;
font-size: 12px;
font-weight: 500;
line-height: 16px;
}
&__author-link {
display: inline-flex;
align-items: center;
gap: 4px;
color: $primary-text-color;
font-weight: 500;
text-decoration: none;
&:hover,
&:active,
&:focus {
color: $highlight-text-color;
}
}
} }
strong { strong {
@ -8885,14 +8940,14 @@ noscript {
} }
.server-banner { .server-banner {
padding: 20px 0;
&__introduction { &__introduction {
font-size: 15px;
line-height: 22px;
color: $darker-text-color; color: $darker-text-color;
margin-bottom: 20px; margin-bottom: 20px;
strong { strong {
font-weight: 600; font-weight: 700;
} }
a { a {
@ -8920,6 +8975,9 @@ noscript {
} }
&__description { &__description {
font-size: 15px;
line-height: 22px;
color: $darker-text-color;
margin-bottom: 20px; margin-bottom: 20px;
} }
@ -9891,14 +9949,14 @@ noscript {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
padding: 4px 12px; padding: 4px 12px;
background: $ui-base-color; background: var(--surface-variant-background-color);
border-radius: 4px; border-radius: 4px;
font-weight: 500; font-weight: 500;
&:hover, &:hover,
&:focus, &:focus,
&:active { &:active {
background: lighten($ui-base-color, 4%); background: var(--surface-variant-active-background-color);
} }
} }
@ -10229,6 +10287,7 @@ noscript {
} }
.more-from-author { .more-from-author {
box-sizing: border-box;
font-size: 14px; font-size: 14px;
color: $darker-text-color; color: $darker-text-color;
background: var(--surface-background-color); background: var(--surface-background-color);

View file

@ -613,9 +613,10 @@ code {
font-family: inherit; font-family: inherit;
pointer-events: none; pointer-events: none;
cursor: default; cursor: default;
max-width: 140px; max-width: 50%;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
&::after { &::after {
content: ''; content: '';

View file

@ -106,4 +106,6 @@ $font-monospace: 'mastodon-font-monospace' !default;
--background-color: #{darken($ui-base-color, 8%)}; --background-color: #{darken($ui-base-color, 8%)};
--background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)}; --background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
--surface-background-color: #{darken($ui-base-color, 4%)}; --surface-background-color: #{darken($ui-base-color, 4%)};
--surface-variant-background-color: #{$ui-base-color};
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
} }

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
module AccessGrantExtension
extend ActiveSupport::Concern
included do
scope :expired, -> { where.not(expires_in: nil).where('created_at + MAKE_INTERVAL(secs => expires_in) < NOW()') }
scope :revoked, -> { where.not(revoked_at: nil).where(revoked_at: ...Time.now.utc) }
end
end

View file

@ -9,6 +9,10 @@ module AccessTokenExtension
has_many :web_push_subscriptions, class_name: 'Web::PushSubscription', inverse_of: :access_token has_many :web_push_subscriptions, class_name: 'Web::PushSubscription', inverse_of: :access_token
after_commit :push_to_streaming_api after_commit :push_to_streaming_api
scope :expired, -> { where.not(expires_in: nil).where('created_at + MAKE_INTERVAL(secs => expires_in) < NOW()') }
scope :not_revoked, -> { where(revoked_at: nil) }
scope :revoked, -> { where.not(revoked_at: nil).where(revoked_at: ...Time.now.utc) }
end end
def revoke(clock = Time) def revoke(clock = Time)

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::Activity::Flag < ActivityPub::Activity class ActivityPub::Activity::Flag < ActivityPub::Activity
COMMENT_SIZE_LIMIT = 5000
def perform def perform
return if skip_reports? return if skip_reports?
@ -38,6 +40,6 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
end end
def report_comment def report_comment
(@json['content'] || '')[0...5000] (@json['content'] || '')[0...COMMENT_SIZE_LIMIT]
end end
end end

View file

@ -10,7 +10,7 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
protected protected
def perform_query def perform_query
[mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version].compact [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version, libvips_version, imagemagick_version, ffmpeg_version].compact
end end
def mastodon_version def mastodon_version
@ -28,8 +28,8 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
{ {
key: 'ruby', key: 'ruby',
human_key: 'Ruby', human_key: 'Ruby',
value: "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}", value: RUBY_DESCRIPTION,
human_value: RUBY_DESCRIPTION, human_value: "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}",
} }
end end
@ -71,6 +71,45 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
nil nil
end end
def libvips_version
return unless Rails.configuration.x.use_vips
{
key: 'libvips',
human_key: 'libvips',
value: Vips.version_string,
human_value: Vips.version_string,
}
end
def imagemagick_version
return if Rails.configuration.x.use_vips
version = `convert -version`.match(/Version: ImageMagick ([\d\.]+)/)[1]
{
key: 'imagemagick',
human_key: 'ImageMagick',
value: version,
human_value: version,
}
rescue Errno::ENOENT
nil
end
def ffmpeg_version
version = `ffmpeg -version`.match(/ffmpeg version ([\d\.]+)/)[1]
{
key: 'ffmpeg',
human_key: 'FFmpeg',
value: version,
human_value: version,
}
rescue Errno::ENOENT
nil
end
def redis_info def redis_info
@redis_info ||= if redis.is_a?(Redis::Namespace) @redis_info ||= if redis.is_a?(Redis::Namespace)
redis.redis.info redis.redis.info

View file

@ -43,7 +43,7 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure
SELECT count(*) FROM new_accounts SELECT count(*) FROM new_accounts
) AS value ) AS value
FROM ( FROM (
SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period #{generated_series_days}
) AS axis ) AS axis
SQL SQL
end end

View file

@ -44,7 +44,7 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur
SELECT count(*) FROM new_followers SELECT count(*) FROM new_followers
) AS value ) AS value
FROM ( FROM (
SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period #{generated_series_days}
) AS axis ) AS axis
SQL SQL
end end

View file

@ -44,7 +44,7 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure:
SELECT count(*) FROM new_follows SELECT count(*) FROM new_follows
) AS value ) AS value
FROM ( FROM (
SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period #{generated_series_days}
) AS axis ) AS axis
SQL SQL
end end

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