Merge pull request #2522 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to b2c5b20ef2
This commit is contained in:
Claire 2023-12-19 18:20:34 +01:00 committed by GitHub
commit 4fdbffc57c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
311 changed files with 20721 additions and 15251 deletions

View file

@ -4,7 +4,7 @@ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
# Install Rails # Install Rails
# RUN gem install rails webdrivers # RUN gem install rails webdrivers
ARG NODE_VERSION="16" ARG NODE_VERSION="20"
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
# [Optional] Uncomment this section to install additional OS packages. # [Optional] Uncomment this section to install additional OS packages.
@ -15,6 +15,6 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
RUN gem install foreman RUN gem install foreman
# [Optional] Uncomment this line to install global node packages. # [Optional] Uncomment this line to install global node packages.
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1 RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && corepack enable" 2>&1
COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt

View file

@ -11,7 +11,8 @@ bundle install
git checkout -- Gemfile.lock git checkout -- Gemfile.lock
# Fetch Javascript dependencies # Fetch Javascript dependencies
yarn --frozen-lockfile corepack prepare
yarn install --immutable
# [re]create, migrate, and seed the test database # [re]create, migrate, and seed the test database
RAILS_ENV=test ./bin/rails db:setup RAILS_ENV=test ./bin/rails db:setup

View file

@ -11,9 +11,32 @@ runs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
cache: yarn
node-version-file: '.nvmrc' node-version-file: '.nvmrc'
# The following is needed because we can not use `cache: true` for `setup-node`, as it does not support Corepack yet and mess up with the cache location if ran after Node is installed
- name: Enable corepack
shell: bash
run: corepack enable
- name: Get yarn cache directory path
id: yarn-cache-dir-path
shell: bash
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install all yarn packages - name: Install all yarn packages
shell: bash shell: bash
run: yarn --frozen-lockfile ${{ inputs.onlyProduction != 'false' && '--production' || '' }} run: yarn install --immutable
if: inputs.onlyProduction == 'false'
- name: Install all production yarn packages
shell: bash
run: yarn workspaces focus --production
if: inputs.onlyProduction != 'false'

View file

@ -12,6 +12,7 @@
// If we do not want a package to be grouped with others, we need to set its groupName // If we do not want a package to be grouped with others, we need to set its groupName
// to `null` after any other rule set it to something. // to `null` after any other rule set it to something.
dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).', dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).',
postUpdateOptions: ['yarnDedupeHighest'],
packageRules: [ packageRules: [
{ {
// Require Dependency Dashboard Approval for major version bumps of these node packages // Require Dependency Dashboard Approval for major version bumps of these node packages

9
.gitignore vendored
View file

@ -55,6 +55,15 @@ npm-debug.log
yarn-error.log yarn-error.log
yarn-debug.log yarn-debug.log
# From https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# Ignore vagrant log files # Ignore vagrant log files
*-cloudimg-console.log *-cloudimg-console.log

View file

@ -31,4 +31,3 @@ linters:
- 'app/views/admin/accounts/_buttons.html.haml' - 'app/views/admin/accounts/_buttons.html.haml'
- 'app/views/admin/accounts/_local_account.html.haml' - 'app/views/admin/accounts/_local_account.html.haml'
- 'app/views/admin/roles/_form.html.haml' - 'app/views/admin/roles/_form.html.haml'
- 'app/views/layouts/application.html.haml'

View file

@ -24,15 +24,6 @@ Lint/NonLocalExitFromIterator:
Exclude: Exclude:
- 'app/helpers/jsonld_helper.rb' - 'app/helpers/jsonld_helper.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
Exclude:
- 'config/initializers/content_security_policy.rb'
- 'config/initializers/doorkeeper.rb'
- 'config/initializers/paperclip.rb'
- 'config/initializers/simple_form.rb'
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize: Metrics/AbcSize:
Max: 144 Max: 144
@ -52,48 +43,10 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 27 Max: 27
RSpec/AnyInstance:
Exclude:
- 'spec/controllers/activitypub/inboxes_controller_spec.rb'
- 'spec/controllers/admin/accounts_controller_spec.rb'
- 'spec/controllers/admin/resets_controller_spec.rb'
- 'spec/controllers/admin/settings/branding_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb'
- 'spec/lib/request_spec.rb'
- 'spec/lib/status_filter_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/setting_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb'
- 'spec/validators/follow_limit_validator_spec.rb'
- 'spec/workers/activitypub/delivery_worker_spec.rb'
- 'spec/workers/web/push_notification_worker_spec.rb'
# Configuration parameters: CountAsOne. # Configuration parameters: CountAsOne.
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 22 Max: 22
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
Exclude:
- 'spec/controllers/api/v1/streaming_controller_spec.rb'
- 'spec/controllers/auth/confirmations_controller_spec.rb'
- 'spec/controllers/auth/passwords_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb'
- 'spec/controllers/concerns/export_controller_concern_spec.rb'
- 'spec/controllers/home_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb'
- 'spec/controllers/statuses_cleanup_controller_spec.rb'
- 'spec/models/concerns/account_finder_concern_spec.rb'
- 'spec/models/concerns/account_interactions_spec.rb'
- 'spec/models/public_feed_spec.rb'
- 'spec/serializers/activitypub/note_serializer_spec.rb'
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
- 'spec/services/remove_status_service_spec.rb'
- 'spec/services/search_service_spec.rb'
- 'spec/services/unblock_domain_service_spec.rb'
RSpec/LetSetup: RSpec/LetSetup:
Exclude: Exclude:
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
@ -136,12 +89,6 @@ RSpec/LetSetup:
- 'spec/services/unsuspend_account_service_spec.rb' - 'spec/services/unsuspend_account_service_spec.rb'
- 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb'
RSpec/MessageChain:
Exclude:
- 'spec/models/concerns/remotable_spec.rb'
- 'spec/models/session_activation_spec.rb'
- 'spec/models/setting_spec.rb'
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 8 Max: 8
@ -181,11 +128,6 @@ Rails/HasManyOrHasOneDependent:
- 'app/models/user.rb' - 'app/models/user.rb'
- 'app/models/web/push_subscription.rb' - 'app/models/web/push_subscription.rb'
Rails/I18nLocaleTexts:
Exclude:
- 'lib/tasks/mastodon.rake'
- 'spec/helpers/flashes_helper_spec.rb'
# Configuration parameters: Include. # Configuration parameters: Include.
# Include: app/controllers/**/*.rb, app/mailers/**/*.rb # Include: app/controllers/**/*.rb, app/mailers/**/*.rb
Rails/LexicallyScopedActionFilter: Rails/LexicallyScopedActionFilter:
@ -561,14 +503,6 @@ Style/SingleArgumentDig:
Exclude: Exclude:
- 'lib/webpacker/manifest_extensions.rb' - 'lib/webpacker/manifest_extensions.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_parentheses, require_no_parentheses
Style/StabbyLambdaParentheses:
Exclude:
- 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
Style/StderrPuts: Style/StderrPuts:
Exclude: Exclude:
@ -627,5 +561,3 @@ Style/TrailingCommaInHashLiteral:
Style/WordArray: Style/WordArray:
Exclude: Exclude:
- 'app/helpers/languages_helper.rb' - 'app/helpers/languages_helper.rb'
- 'spec/controllers/settings/imports_controller_spec.rb'
- 'spec/models/form/import_spec.rb'

3
.watchmanconfig Normal file
View file

@ -0,0 +1,3 @@
{
"ignore_dirs": ["node_modules/", "public/"]
}

0
.yarn/.gitkeep Normal file
View file

View file

@ -0,0 +1,13 @@
diff --git a/lib/index.js b/lib/index.js
index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -99,7 +99,7 @@ function lodash(_ref) {
var node = _ref3;
- if ((0, _types.isModuleDeclaration)(node)) {
+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) {
isModule = true;
break;
}

View file

@ -0,0 +1,22 @@
diff --git a/dist/index.js b/dist/index.js
index 57e375592d984e9a429bcd9f800fa2d15cd662e4..0c47d96df3608e23adfd77d887a8f72abbd501c0 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
});
exports.default = void 0;
-var _crypto = _interopRequireDefault(require("crypto"));
+var _createHash = _interopRequireDefault(require("webpack/lib/util/createHash"));
var _path = _interopRequireDefault(require("path"));
@@ -227,7 +227,7 @@ class CompressionPlugin {
originalAlgorithm: this.options.algorithm,
compressionOptions: this.options.compressionOptions,
name,
- contentHash: _crypto.default.createHash("md4").update(input).digest("hex")
+ contentHash: _createHash.default("md4").update(input).digest("hex")
};
} else {
cacheData.name = (0, _serializeJavascript.default)({

View file

@ -1,49 +0,0 @@
# test directories
__tests__
test
tests
powered-test
# asset directories
docs
doc
website
images
# assets
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
.*.yml
*.yml
# misc
*.gz
*.md
# for specific ignore
!.svgo.yml
!sass-lint/**/*.yml
# breaks lint-staged or generally anything using https://github.com/eemeli/yaml/issues/384
!**/yaml/dist/**/doc

1
.yarnrc.yml Normal file
View file

@ -0,0 +1 @@
nodeLinker: node-modules

View file

@ -13,7 +13,6 @@ ENV DEBIAN_FRONTEND="noninteractive" \
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
WORKDIR /opt/mastodon WORKDIR /opt/mastodon
COPY Gemfile* package.json yarn.lock /opt/mastodon/
# hadolint ignore=DL3008 # hadolint ignore=DL3008
RUN apt-get update && \ RUN apt-get update && \
@ -36,8 +35,14 @@ RUN apt-get update && \
bundle config set --local deployment 'true' && \ bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \ bundle config set --local without 'development test' && \
bundle config set silence_root_warning true && \ bundle config set silence_root_warning true && \
bundle install -j"$(nproc)" && \ corepack enable
yarn install --pure-lockfile --production --network-timeout 600000 && \
COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/
COPY .yarn /opt/mastodon/.yarn
RUN bundle install -j"$(nproc)"
RUN yarn workspaces focus --all --production && \
yarn cache clean yarn cache clean
FROM node:${NODE_VERSION} FROM node:${NODE_VERSION}
@ -78,7 +83,8 @@ RUN apt-get update && \
tzdata \ tzdata \
libreadline8 \ libreadline8 \
tini && \ tini && \
ln -s /opt/mastodon /mastodon ln -s /opt/mastodon /mastodon && \
corepack enable
# Note: no, cleaning here since Debian does this automatically # Note: no, cleaning here since Debian does this automatically
# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem # See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem

View file

@ -16,7 +16,7 @@ gem 'dotenv-rails', '~> 2.8'
gem 'aws-sdk-s3', '~> 1.123', require: false gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'fog-core', '<= 2.4.0' gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 1.0', require: false
gem 'kt-paperclip', '~> 7.2' gem 'kt-paperclip', '~> 7.2'
gem 'md-paperclip-azure', '~> 2.2', require: false gem 'md-paperclip-azure', '~> 2.2', require: false
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
@ -88,7 +88,7 @@ gem 'simple-navigation', '~> 4.4'
gem 'simple_form', '~> 5.2' gem 'simple_form', '~> 5.2'
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
gem 'stoplight', '~> 3.0.1' gem 'stoplight', '~> 3.0.1'
gem 'strong_migrations', '1.3.0' gem 'strong_migrations', '1.6.4'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023' gem 'tzinfo-data', '~> 1.2023'
@ -195,7 +195,7 @@ gem 'xorcist', '~> 1.1'
gem 'cocoon', '~> 1.2' gem 'cocoon', '~> 1.2'
gem 'net-http', '~> 0.3.2' gem 'net-http', '~> 0.4.0'
gem 'rubyzip', '~> 2.3' gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1' gem 'hcaptcha', '~> 7.1'

View file

@ -39,50 +39,51 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.1.1) actioncable (7.1.2)
actionpack (= 7.1.1) actionpack (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
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.1) actionmailbox (7.1.2)
actionpack (= 7.1.1) actionpack (= 7.1.2)
activejob (= 7.1.1) activejob (= 7.1.2)
activerecord (= 7.1.1) activerecord (= 7.1.2)
activestorage (= 7.1.1) activestorage (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.1) actionmailer (7.1.2)
actionpack (= 7.1.1) actionpack (= 7.1.2)
actionview (= 7.1.1) actionview (= 7.1.2)
activejob (= 7.1.1) activejob (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.1.1) actionpack (7.1.2)
actionview (= 7.1.1) actionview (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
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.1) actiontext (7.1.2)
actionpack (= 7.1.1) actionpack (= 7.1.2)
activerecord (= 7.1.1) activerecord (= 7.1.2)
activestorage (= 7.1.1) activestorage (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.1) actionview (7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
@ -92,22 +93,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.1) activejob (7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.1) activemodel (7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
activerecord (7.1.1) activerecord (7.1.2)
activemodel (= 7.1.1) activemodel (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.1) activestorage (7.1.2)
actionpack (= 7.1.1) actionpack (= 7.1.2)
activejob (= 7.1.1) activejob (= 7.1.2)
activerecord (= 7.1.1) activerecord (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.1) activesupport (7.1.2)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@ -130,8 +131,8 @@ GEM
attr_required (1.0.1) attr_required (1.0.1)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.809.0) aws-partitions (1.828.0)
aws-sdk-core (3.181.0) aws-sdk-core (3.183.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
@ -139,7 +140,7 @@ GEM
aws-sdk-kms (1.71.0) aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.133.0) aws-sdk-s3 (1.136.0)
aws-sdk-core (~> 3, >= 3.181.0) aws-sdk-core (~> 3, >= 3.181.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6) aws-sigv4 (~> 1.6)
@ -218,7 +219,7 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.3.3) date (3.3.4)
debug_inspector (1.1.0) debug_inspector (1.1.0)
devise (4.9.3) devise (4.9.3)
bcrypt (~> 3.0) bcrypt (~> 3.0)
@ -263,7 +264,7 @@ GEM
erubi (1.12.0) erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
excon (0.100.0) excon (0.104.0)
fabrication (2.30.0) fabrication (2.30.0)
faker (3.2.2) faker (3.2.2)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
@ -298,19 +299,18 @@ GEM
ffi-compiler (1.0.1) ffi-compiler (1.0.1)
ffi (>= 1.0.0) ffi (>= 1.0.0)
rake rake
fog-core (2.1.0) fog-core (2.3.0)
builder builder
excon (~> 0.58) excon (~> 0.71)
formatador (~> 0.2) formatador (>= 0.2, < 2.0)
mime-types mime-types
fog-json (1.2.0) fog-json (1.2.0)
fog-core fog-core
multi_json (~> 1.10) multi_json (~> 1.10)
fog-openstack (0.3.10) fog-openstack (1.1.0)
fog-core (>= 1.45, <= 2.1.0) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) formatador (1.1.0)
formatador (0.3.0)
fugit (1.8.1) fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
@ -370,8 +370,7 @@ GEM
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.5) idn-ruby (0.1.5)
io-console (0.6.0) io-console (0.6.0)
ipaddress (0.8.3) irb (1.8.3)
irb (1.8.1)
rdoc rdoc
reline (>= 0.3.8) reline (>= 0.3.8)
jmespath (1.6.2) jmespath (1.6.2)
@ -389,10 +388,10 @@ GEM
multi_json (~> 1.15) multi_json (~> 1.15)
rack (>= 2.2, < 4) rack (>= 2.2, < 4)
rdf (~> 3.3) rdf (~> 3.3)
json-ld-preloaded (3.2.2) json-ld-preloaded (3.3.0)
json-ld (~> 3.2) json-ld (~> 3.3)
rdf (~> 3.2) rdf (~> 3.3)
json-schema (4.0.0) json-schema (4.1.1)
addressable (>= 2.8) addressable (>= 2.8)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.7.1) jwt (2.7.1)
@ -452,25 +451,25 @@ GEM
memory_profiler (1.0.1) memory_profiler (1.0.1)
mime-types (3.5.1) mime-types (3.5.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.0808) mime-types-data (3.2023.1003)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.4) mini_portile2 (2.8.5)
minitest (5.20.0) minitest (5.20.0)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
mutex_m (0.1.2) mutex_m (0.1.2)
net-http (0.3.2) net-http (0.4.0)
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.1) net-imap (0.4.4)
date date
net-protocol net-protocol
net-ldap (0.18.0) net-ldap (0.18.0)
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.1) net-protocol (0.2.2)
timeout timeout
net-smtp (0.4.0) net-smtp (0.4.0)
net-protocol net-protocol
@ -528,7 +527,7 @@ GEM
net-smtp net-smtp
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0) private_address_check (0.5.0)
psych (5.1.1) psych (5.1.1.1)
stringio stringio
public_suffix (5.0.3) public_suffix (5.0.3)
puma (6.4.0) puma (6.4.0)
@ -559,20 +558,20 @@ GEM
rackup (1.0.0) rackup (1.0.0)
rack (< 3) rack (< 3)
webrick webrick
rails (7.1.1) rails (7.1.2)
actioncable (= 7.1.1) actioncable (= 7.1.2)
actionmailbox (= 7.1.1) actionmailbox (= 7.1.2)
actionmailer (= 7.1.1) actionmailer (= 7.1.2)
actionpack (= 7.1.1) actionpack (= 7.1.2)
actiontext (= 7.1.1) actiontext (= 7.1.2)
actionview (= 7.1.1) actionview (= 7.1.2)
activejob (= 7.1.1) activejob (= 7.1.2)
activemodel (= 7.1.1) activemodel (= 7.1.2)
activerecord (= 7.1.1) activerecord (= 7.1.2)
activestorage (= 7.1.1) activestorage (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.1) railties (= 7.1.2)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
@ -587,9 +586,9 @@ GEM
rails-i18n (7.0.8) rails-i18n (7.0.8)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
railties (7.1.1) railties (7.1.2)
actionpack (= 7.1.1) actionpack (= 7.1.2)
activesupport (= 7.1.1) activesupport (= 7.1.2)
irb irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -689,13 +688,13 @@ GEM
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.0.2) sanitize (6.1.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
scenic (1.7.0) scenic (1.7.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
selenium-webdriver (4.13.1) selenium-webdriver (4.15.0)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
@ -710,7 +709,7 @@ GEM
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 6, < 8)
tilt (>= 1.4.0) tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.29) sidekiq-unique-jobs (7.1.30)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0) brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
redis (< 5.0) redis (< 5.0)
@ -718,7 +717,7 @@ GEM
thor (>= 0.20, < 3.0) thor (>= 0.20, < 3.0)
simple-navigation (4.4.0) simple-navigation (4.4.0)
activesupport (>= 2.3.2) activesupport (>= 2.3.2)
simple_form (5.2.0) simple_form (5.3.0)
actionpack (>= 5.2) actionpack (>= 5.2)
activemodel (>= 5.2) activemodel (>= 5.2)
simplecov (0.22.0) simplecov (0.22.0)
@ -739,8 +738,8 @@ GEM
statsd-ruby (1.5.0) statsd-ruby (1.5.0)
stoplight (3.0.2) stoplight (3.0.2)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.0.8) stringio (3.0.9)
strong_migrations (1.3.0) strong_migrations (1.6.4)
activerecord (>= 5.2) activerecord (>= 5.2)
swd (1.3.0) swd (1.3.0)
activesupport (>= 3) activesupport (>= 3)
@ -753,9 +752,9 @@ GEM
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
test-prof (1.2.3) test-prof (1.2.3)
thor (1.2.2) thor (1.3.0)
tilt (2.3.0) tilt (2.3.0)
timeout (0.4.0) timeout (0.4.1)
tpm-key_attestation (0.12.0) tpm-key_attestation (0.12.0)
bindata (~> 2.4) bindata (~> 2.4)
openssl (> 2.0) openssl (> 2.0)
@ -858,7 +857,7 @@ DEPENDENCIES
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (<= 2.4.0) fog-core (<= 2.4.0)
fog-openstack (~> 0.3) fog-openstack (~> 1.0)
fuubar (~> 2.5) fuubar (~> 2.5)
haml-rails (~> 2.0) haml-rails (~> 2.0)
haml_lint haml_lint
@ -883,7 +882,7 @@ DEPENDENCIES
md-paperclip-azure (~> 2.2) md-paperclip-azure (~> 2.2)
memory_profiler memory_profiler
mime-types (~> 3.5.0) mime-types (~> 3.5.0)
net-http (~> 0.3.2) net-http (~> 0.4.0)
net-ldap (~> 0.18) net-ldap (~> 0.18)
nokogiri (~> 1.15) nokogiri (~> 1.15)
nsa! nsa!
@ -941,7 +940,7 @@ DEPENDENCIES
sprockets-rails (~> 3.4) sprockets-rails (~> 3.4)
stackprof stackprof
stoplight (~> 3.0.1) stoplight (~> 3.0.1)
strong_migrations (= 1.3.0) strong_migrations (= 1.6.4)
test-prof test-prof
thor (~> 1.2) thor (~> 1.2)
tty-prompt (~> 0.23) tty-prompt (~> 0.23)

4
Vagrantfile vendored
View file

@ -112,11 +112,11 @@ bundle install
# Install node modules # Install node modules
sudo corepack enable sudo corepack enable
yarn set version classic corepack prepare
yarn install yarn install
# Build Mastodon # Build Mastodon
export RAILS_ENV=development export RAILS_ENV=development
export $(cat ".env.vagrant" | xargs) export $(cat ".env.vagrant" | xargs)
bundle exec rails db:setup bundle exec rails db:setup

View file

@ -53,7 +53,7 @@ class PublicStatusesIndex < Chewy::Index
index_scope ::Status.unscoped index_scope ::Status.unscoped
.kept .kept
.indexable .indexable
.includes(:media_attachments, :preloadable_poll, :preview_cards, :tags) .includes(:media_attachments, :preloadable_poll, :tags, preview_cards_status: :preview_card)
root date_detection: false do root date_detection: false do
field(:id, type: 'long') field(:id, type: 'long')

View file

@ -50,7 +50,7 @@ class StatusesIndex < Chewy::Index
}, },
} }
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preview_cards, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, :tags, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? } index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, :tags, preview_cards_status: :preview_card, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? }
root date_detection: false do root date_detection: false do
field(:id, type: 'long') field(:id, type: 'long')

View file

@ -31,6 +31,11 @@ module Admin
private private
def batched_ordered_status_edits
@status.edits.reorder(nil).includes(:account, status: [:account]).find_each(order: :asc)
end
helper_method :batched_ordered_status_edits
def admin_status_batch_action_params def admin_status_batch_action_params
params.require(:admin_status_batch_action).permit(status_ids: []) params.require(:admin_status_batch_action).permit(status_ids: [])
end end

View file

@ -7,6 +7,7 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders include RateLimitHeaders
include AccessTokenTrackingConcern include AccessTokenTrackingConcern
include ApiCachingConcern include ApiCachingConcern
include Api::ContentSecurityPolicy
skip_before_action :require_functional!, unless: :limited_federation_mode? skip_before_action :require_functional!, unless: :limited_federation_mode?
@ -17,26 +18,6 @@ class Api::BaseController < ApplicationController
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
content_security_policy do |p|
# Set every directive that does not have a fallback
p.default_src :none
p.frame_ancestors :none
p.form_action :none
# Disable every directive with a fallback to cut on response size
p.base_uri false
p.font_src false
p.img_src false
p.style_src false
p.media_src false
p.frame_src false
p.manifest_src false
p.connect_src false
p.script_src false
p.child_src false
p.worker_src false
end
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
render json: { error: e.to_s }, status: 422 render json: { error: e.to_s }, status: 422
end end

View file

@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action :require_user! before_action :require_user!
def index def index
accounts = Account.where(id: account_ids).select('id') scope = Account.where(id: account_ids).select('id')
scope.merge!(Account.without_suspended) unless truthy_param?(:with_suspended)
# .where doesn't guarantee that our results are in the same order # .where doesn't guarantee that our results are in the same order
# we requested them, so return the "right" order to the requestor. # we requested them, so return the "right" order to the requestor.
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact @accounts = scope.index_by(&:id).values_at(*account_ids).compact
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
end end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::AccountsController < Api::BaseController class Api::V1::AccountsController < Api::BaseController
include RegistrationHelper
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute] before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute]
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers] before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
before_action -> { doorkeeper_authorize! :follow, :write, :'write:mutes' }, only: [:mute, :unmute] before_action -> { doorkeeper_authorize! :follow, :write, :'write:mutes' }, only: [:mute, :unmute]
@ -90,18 +92,14 @@ class Api::V1::AccountsController < Api::BaseController
end end
def account_params def account_params
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone) params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
end
def invite
Invite.find_by(code: params[:invite_code]) if params[:invite_code].present?
end end
def check_enabled_registrations def check_enabled_registrations
forbidden if single_user_mode? || omniauth_only? || !allowed_registrations? forbidden unless allowed_registration?(request.remote_ip, invite)
end
def allowed_registrations?
Setting.registrations_mode != 'none'
end
def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end end
end end

View file

@ -41,10 +41,10 @@ class Api::V1::ConversationsController < Api::BaseController
account: :account_stat, account: :account_stat,
last_status: [ last_status: [
:media_attachments, :media_attachments,
:preview_cards,
:status_stat, :status_stat,
:tags, :tags,
{ {
preview_cards_status: :preview_card,
active_mentions: [account: :account_stat], active_mentions: [account: :account_stat],
account: :account_stat, account: :account_stat,
}, },

View file

@ -1,12 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::ActivityController < Api::BaseController class Api::V1::Instances::ActivityController < Api::V1::Instances::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
vary_by ''
def show def show
cache_even_if_authenticated! cache_even_if_authenticated!
render_with_cache json: :activity, expires_in: 1.day render_with_cache json: :activity, expires_in: 1.day

View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
class Api::V1::Instances::BaseController < Api::BaseController
skip_before_action :require_authenticated_user!,
unless: :limited_federation_mode?
vary_by ''
end

View file

@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::DomainBlocksController < Api::BaseController class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
before_action :require_enabled_api! before_action :require_enabled_api!
before_action :set_domain_blocks before_action :set_domain_blocks
@ -15,7 +13,7 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
cache_if_unauthenticated! cache_if_unauthenticated!
end end
render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: show_rationale_in_response?
end end
private private
@ -27,4 +25,16 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
def set_domain_blocks def set_domain_blocks
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity @domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
end end
def show_rationale_in_response?
always_show_rationale? || show_rationale_for_user?
end
def always_show_rationale?
Setting.show_domain_blocks_rationale == 'all'
end
def show_rationale_for_user?
Setting.show_domain_blocks_rationale == 'users' && user_signed_in?
end
end end

View file

@ -1,13 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController class Api::V1::Instances::ExtendedDescriptionsController < Api::V1::Instances::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale skip_around_action :set_locale
before_action :set_extended_description before_action :set_extended_description
vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode # Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user def current_user
super if limited_federation_mode? super if limited_federation_mode?

View file

@ -1,13 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::LanguagesController < Api::BaseController class Api::V1::Instances::LanguagesController < Api::V1::Instances::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale skip_around_action :set_locale
before_action :set_languages before_action :set_languages
vary_by ''
def show def show
cache_even_if_authenticated! cache_even_if_authenticated!
render json: @languages, each_serializer: REST::LanguageSerializer render json: @languages, each_serializer: REST::LanguageSerializer

View file

@ -1,13 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::PeersController < Api::BaseController class Api::V1::Instances::PeersController < Api::V1::Instances::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale skip_around_action :set_locale
vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode # Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user def current_user
super if limited_federation_mode? super if limited_federation_mode?

View file

@ -1,12 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController class Api::V1::Instances::PrivacyPoliciesController < Api::V1::Instances::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
before_action :set_privacy_policy before_action :set_privacy_policy
vary_by ''
def show def show
cache_even_if_authenticated! cache_even_if_authenticated!
render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer

View file

@ -1,13 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::RulesController < Api::BaseController class Api::V1::Instances::RulesController < Api::V1::Instances::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale skip_around_action :set_locale
before_action :set_rules before_action :set_rules
vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode # Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user def current_user
super if limited_federation_mode? super if limited_federation_mode?

View file

@ -1,12 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::TranslationLanguagesController < Api::BaseController class Api::V1::Instances::TranslationLanguagesController < Api::V1::Instances::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
before_action :set_languages before_action :set_languages
vary_by ''
def show def show
cache_even_if_authenticated! cache_even_if_authenticated!
render json: @languages render json: @languages

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
class Api::V1::InvitesController < Api::BaseController
include RegistrationHelper
skip_before_action :require_authenticated_user!
skip_around_action :set_locale
before_action :set_invite
before_action :check_enabled_registrations!
# Override `current_user` to avoid reading session cookies
def current_user; end
def show
render json: { invite_code: params[:invite_code], instance_api_url: api_v2_instance_url }, status: 200
end
private
def set_invite
@invite = Invite.find_by!(code: params[:invite_code])
end
def check_enabled_registrations!
return render json: { error: I18n.t('invites.invalid') }, status: 401 unless @invite.valid_for_use?
raise Mastodon::NotPermittedError unless allowed_registration?(request.remote_ip, @invite)
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class Api::V1::Statuses::BaseController < Api::BaseController
include Authorization
before_action :set_status
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end

View file

@ -1,11 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::BookmarksController < Api::BaseController class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' } before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
before_action :require_user! before_action :require_user!
before_action :set_status, only: [:create] skip_before_action :set_status, only: [:destroy]
def create def create
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status) current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
@ -28,13 +26,4 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
not_found not_found
end end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end end

View file

@ -1,10 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_status
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
@ -61,13 +58,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end

View file

@ -1,11 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::FavouritesController < Api::BaseController class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:favourites' } before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
before_action :require_user! before_action :require_user!
before_action :set_status, only: [:create] skip_before_action :set_status, only: [:destroy]
def create def create
FavouriteService.new.call(current_account, @status) FavouriteService.new.call(current_account, @status)
@ -30,13 +28,4 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
rescue Mastodon::NotPermittedError rescue Mastodon::NotPermittedError
not_found not_found
end end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end end

View file

@ -1,10 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::HistoriesController < Api::BaseController class Api::V1::Statuses::HistoriesController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_status
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
@ -16,11 +13,4 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
def status_edits def status_edits
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
end end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end end

View file

@ -1,11 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::MutesController < Api::BaseController class Api::V1::Statuses::MutesController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:mutes' } before_action -> { doorkeeper_authorize! :write, :'write:mutes' }
before_action :require_user! before_action :require_user!
before_action :set_status
before_action :set_conversation before_action :set_conversation
def create def create
@ -24,13 +21,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController
private private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def set_conversation def set_conversation
@conversation = @status.conversation @conversation = @status.conversation
raise Mastodon::ValidationError if @conversation.nil? raise Mastodon::ValidationError if @conversation.nil?

View file

@ -1,11 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::PinsController < Api::BaseController class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :write, :'write:accounts' } before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user! before_action :require_user!
before_action :set_status
def create def create
StatusPin.create!(account: current_account, status: @status) StatusPin.create!(account: current_account, status: @status)
@ -26,10 +23,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
private private
def set_status
@status = Status.find(params[:status_id])
end
def distribute_add_activity! def distribute_add_activity!
json = ActiveModelSerializers::SerializableResource.new( json = ActiveModelSerializers::SerializableResource.new(
@status, @status,

View file

@ -1,10 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { authorize_if_got_token! :read, :'read:accounts' } before_action -> { authorize_if_got_token! :read, :'read:accounts' }
before_action :set_status
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
@ -57,13 +54,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) @accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end end
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end

View file

@ -1,13 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::ReblogsController < Api::BaseController class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
include Authorization
include Redisable include Redisable
include Lockable include Lockable
before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action :require_user! before_action :require_user!
before_action :set_reblog, only: [:create] before_action :set_reblog, only: [:create]
skip_before_action :set_status
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses

View file

@ -1,21 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::SourcesController < Api::BaseController class Api::V1::Statuses::SourcesController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read, :'read:statuses' } before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :set_status
def show def show
render json: @status, serializer: REST::StatusSourceSerializer render json: @status, serializer: REST::StatusSourceSerializer
end end
private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end end

View file

@ -1,10 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Statuses::TranslationsController < Api::BaseController class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
include Authorization
before_action -> { doorkeeper_authorize! :read, :'read:statuses' } before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :set_status
before_action :set_translation before_action :set_translation
rescue_from TranslationService::NotConfiguredError, with: :not_found rescue_from TranslationService::NotConfiguredError, with: :not_found
@ -24,13 +21,6 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
private private
def set_status
@status = Status.find(params[:status_id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
def set_translation def set_translation
@translation = TranslateStatusService.new.call(@status, content_locale) @translation = TranslateStatusService.new.call(@status, content_locale)
end end

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Auth::RegistrationsController < Devise::RegistrationsController class Auth::RegistrationsController < Devise::RegistrationsController
include RegistrationHelper
include RegistrationSpamConcern include RegistrationSpamConcern
layout :determine_layout layout :determine_layout
@ -83,19 +84,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end
def check_enabled_registrations def check_enabled_registrations
redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations? || ip_blocked? redirect_to root_path unless allowed_registration?(request.remote_ip, @invite)
end
def allowed_registrations?
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
end
def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end
def ip_blocked?
IpBlock.where(severity: :sign_up_block).where('ip >>= ?', request.remote_ip.to_s).exists?
end end
def invite_code def invite_code

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Api::ContentSecurityPolicy
extend ActiveSupport::Concern
included do
content_security_policy do |policy|
# Set every directive that does not have a fallback
policy.default_src :none
policy.frame_ancestors :none
policy.form_action :none
# Disable every directive with a fallback to cut on response size
policy.base_uri false
policy.font_src false
policy.img_src false
policy.style_src false
policy.media_src false
policy.frame_src false
policy.manifest_src false
policy.connect_src false
policy.script_src false
policy.child_src false
policy.worker_src false
end
end
end

View file

@ -6,7 +6,7 @@ module Admin::AccountModerationNotesHelper
link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
safe_join([ safe_join([
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'),
content_tag(:span, account.acct, class: 'username'), content_tag(:span, account.acct, class: 'username'),
], ' ') ], ' ')
end end

View file

@ -91,6 +91,14 @@ module ApplicationHelper
end end
end end
def html_title
safe_join(
[content_for(:page_title).to_s.chomp, title]
.select(&:present?),
' - '
)
end
def title def title
Rails.env.production? ? site_title : "#{site_title} (Dev)" Rails.env.production? ? site_title : "#{site_title} (Dev)"
end end

View file

@ -298,5 +298,3 @@ module LanguagesHelper
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
end end
end end
# rubocop:enable Metrics/ModuleLength

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
module RegistrationHelper
extend ActiveSupport::Concern
def allowed_registration?(remote_ip, invite)
!Rails.configuration.x.single_user_mode && !omniauth_only? && (registrations_open? || invite&.valid_for_use?) && !ip_blocked?(remote_ip)
end
def registrations_open?
Setting.registrations_mode != 'none'
end
def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end
def ip_blocked?(remote_ip)
IpBlock.where(severity: :sign_up_block).exists?(['ip >>= ?', remote_ip.to_s])
end
end

View file

@ -25,7 +25,7 @@ module SettingsHelper
return if account.nil? return if account.nil?
link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do link_to ActivityPub::TagManager.instance.url_for(account), class: 'name-tag', title: account.acct do
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
end end
end end
end end

View file

@ -567,7 +567,7 @@ export function fetchRelationships(accountIds) {
dispatch(fetchRelationshipsRequest(newAccountIds)); dispatch(fetchRelationshipsRequest(newAccountIds));
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess(response.data)); dispatch(fetchRelationshipsSuccess(response.data));
}).catch(error => { }).catch(error => {
dispatch(fetchRelationshipsFail(error)); dispatch(fetchRelationshipsFail(error));

View file

@ -119,7 +119,7 @@ class Account extends ImmutablePureComponent {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />; buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
} else if (defaultAction === 'block') { } else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />; buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
} else if (!account.get('moved') || following) { } else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />; buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
} }
} }

View file

@ -209,7 +209,7 @@ class Header extends ImmutablePureComponent {
actionBtn = ''; actionBtn = '';
} }
if (suspended && !account.getIn(['relationship', 'following'])) { if (account.get('suspended') && !account.getIn(['relationship', 'following'])) {
actionBtn = ''; actionBtn = '';
} }
@ -217,7 +217,7 @@ class Header extends ImmutablePureComponent {
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />; lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
} }
if (signedIn && account.get('id') !== me && !suspended) { if (signedIn && account.get('id') !== me && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null); menu.push(null);
@ -228,7 +228,7 @@ class Header extends ImmutablePureComponent {
menu.push(null); menu.push(null);
} }
if ('share' in navigator && !suspended) { if ('share' in navigator && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
menu.push(null); menu.push(null);
} }
@ -276,7 +276,9 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true }); menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
} }
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); if (!account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
}
} }
if (signedIn && isRemote) { if (signedIn && isRemote) {
@ -340,18 +342,16 @@ class Header extends ImmutablePureComponent {
{role} {role}
</a> </a>
{!suspended && ( <div className='account__header__tabs__buttons'>
<div className='account__header__tabs__buttons'> {!hidden && (
{!hidden && ( <>
<> {actionBtn}
{actionBtn} {bellBtn}
{bellBtn} </>
</> )}
)}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' /> <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' />
</div> </div>
)}
</div> </div>
<div className='account__header__tabs__name'> <div className='account__header__tabs__name'>

View file

@ -200,7 +200,7 @@ class ListTimeline extends PureComponent {
</div> </div>
<div className='setting-toggle'> <div className='setting-toggle'>
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} /> <Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'> <label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' /> <FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
</label> </label>

View file

@ -11,6 +11,7 @@ import { throttle } from 'lodash';
import { Blurhash } from 'flavours/glitch/components/blurhash'; import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { playerSettings } from 'flavours/glitch/settings';
import { displayMedia, useBlurhash } from '../../initial_state'; import { displayMedia, useBlurhash } from '../../initial_state';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
@ -221,8 +222,8 @@ class Video extends PureComponent {
if(!isNaN(x)) { if(!isNaN(x)) {
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => { this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
this.video.volume = x; this._syncVideoToVolumeState(x);
this.video.muted = this.state.muted; this._saveVolumeState(x);
}); });
} }
}, 15); }, 15);
@ -360,6 +361,8 @@ class Video extends PureComponent {
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
window.addEventListener('scroll', this.handleScroll); window.addEventListener('scroll', this.handleScroll);
this._syncVideoFromLocalStorage();
} }
componentWillUnmount () { componentWillUnmount () {
@ -432,8 +435,28 @@ class Video extends PureComponent {
const muted = !(this.video.muted || this.state.volume === 0); const muted = !(this.video.muted || this.state.volume === 0);
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => { this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
this.video.volume = this.state.volume; this._syncVideoToVolumeState();
this.video.muted = this.state.muted; this._saveVolumeState();
});
};
_syncVideoToVolumeState = (volume = null, muted = null) => {
if (!this.video) {
return;
}
this.video.volume = volume ?? this.state.volume;
this.video.muted = muted ?? this.state.muted;
};
_saveVolumeState = (volume = null, muted = null) => {
playerSettings.set('volume', volume ?? this.state.volume);
playerSettings.set('muted', muted ?? this.state.muted);
};
_syncVideoFromLocalStorage = () => {
this.setState({ volume: playerSettings.get('volume') ?? 0.5, muted: playerSettings.get('muted') ?? false }, () => {
this._syncVideoToVolumeState();
}); });
}; };
@ -479,6 +502,7 @@ class Video extends PureComponent {
handleVolumeChange = () => { handleVolumeChange = () => {
this.setState({ volume: this.video.volume, muted: this.video.muted }); this.setState({ volume: this.video.volume, muted: this.video.muted });
this._saveVolumeState(this.video.volume, this.video.muted);
}; };
handleOpenVideo = () => { handleOpenVideo = () => {

View file

@ -46,4 +46,5 @@ export default class Settings {
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data'); export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
export const tagHistory = new Settings('mastodon_tag_history'); export const tagHistory = new Settings('mastodon_tag_history');
export const bannerSettings = new Settings('mastodon_banner_settings'); export const bannerSettings = new Settings('mastodon_banner_settings');
export const searchHistory = new Settings('mastodon_search_history'); export const searchHistory = new Settings('mastodon_search_history');
export const playerSettings = new Settings('mastodon_player');

View file

@ -36,6 +36,7 @@
.modal-root__modal { .modal-root__modal {
pointer-events: auto; pointer-events: auto;
user-select: text;
display: flex; display: flex;
} }

View file

@ -460,7 +460,7 @@ export function fetchRelationships(accountIds) {
dispatch(fetchRelationshipsRequest(newAccountIds)); dispatch(fetchRelationshipsRequest(newAccountIds));
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchRelationshipsSuccess({ relationships: response.data })); dispatch(fetchRelationshipsSuccess({ relationships: response.data }));
}).catch(error => { }).catch(error => {
dispatch(fetchRelationshipsFail(error)); dispatch(fetchRelationshipsFail(error));

View file

@ -42,4 +42,5 @@ export interface ApiAccountJSON {
suspended?: boolean; suspended?: boolean;
limited?: boolean; limited?: boolean;
memorial?: boolean; memorial?: boolean;
hide_collections: boolean;
} }

View file

@ -13,7 +13,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
} }
> >
<img <img
alt="alice" alt=""
src="/animated/alice.gif" src="/animated/alice.gif"
/> />
</div> </div>
@ -32,7 +32,7 @@ exports[`<Avatar /> Still renders a still avatar 1`] = `
} }
> >
<img <img
alt="alice" alt=""
src="/static/alice.jpg" src="/static/alice.jpg"
/> />
</div> </div>

View file

@ -119,7 +119,7 @@ class Account extends ImmutablePureComponent {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />; buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
} else if (defaultAction === 'block') { } else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />; buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
} else if (!account.get('moved') || following) { } else if (!account.get('suspended') && !account.get('moved') || following) {
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />; buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
} }
} }

View file

@ -42,7 +42,7 @@ export const Avatar: React.FC<Props> = ({
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
style={style} style={style}
> >
{src && <img src={src} alt={account?.get('acct')} />} {src && <img src={src} alt='' />}
</div> </div>
); );
}; };

View file

@ -19,6 +19,8 @@ import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/s
import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg'; import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg';
import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg'; import { ReactComponent as VisibilityIcon } from '@material-symbols/svg-600/outlined/visibility.svg';
import { ReactComponent as RepeatDisabledIcon } from 'mastodon/../svg-icons/repeat_disabled.svg';
import { ReactComponent as RepeatPrivateIcon } from 'mastodon/../svg-icons/repeat_private.svg';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -348,6 +350,7 @@ class StatusActionBar extends ImmutablePureComponent {
let replyIcon; let replyIcon;
let replyIconComponent; let replyIconComponent;
let replyTitle; let replyTitle;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply'; replyIcon = 'reply';
replyIconComponent = ReplyIcon; replyIconComponent = ReplyIcon;
@ -360,15 +363,20 @@ class StatusActionBar extends ImmutablePureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle = ''; let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) { if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) { } else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog); reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) { } else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private); reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else { } else {
reblogTitle = intl.formatMessage(messages.cannot_reblog); reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
} }
const filterButton = this.props.onFilter && ( const filterButton = this.props.onFilter && (
@ -380,7 +388,7 @@ class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} /> <IconButton className='status__action-bar__button' title={replyTitle} icon={isReply ? 'reply' : replyIcon} iconComponent={isReply ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} /> <IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} /> <IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /> <IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />

View file

@ -0,0 +1,80 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { render, fireEvent } from '@testing-library/react';
class Media extends Component {
constructor(props) {
super(props);
this.state = {
paused: props.paused || false,
};
}
handleMediaClick = () => {
const { onClick } = this.props;
this.setState(prevState => ({
paused: !prevState.paused,
}));
if (typeof onClick === 'function') {
onClick();
}
const { title } = this.props;
const mediaElements = document.querySelectorAll(`div[title="${title}"]`);
setTimeout(() => {
mediaElements.forEach(element => {
if (element !== this && !element.classList.contains('paused')) {
element.click();
}
});
}, 0);
};
render() {
const { title } = this.props;
const { paused } = this.state;
return (
<button title={title} onClick={this.handleMediaClick}>
Media Component - {paused ? 'Paused' : 'Playing'}
</button>
);
}
}
Media.propTypes = {
title: PropTypes.string.isRequired,
onClick: PropTypes.func,
paused: PropTypes.bool,
};
describe('Media attachments test', () => {
let currentMedia = null;
const togglePlayMock = jest.fn();
it('plays a new media file and pauses others that were playing', () => {
const container = render(
<div>
<Media title='firstMedia' paused onClick={togglePlayMock} />
<Media title='secondMedia' paused onClick={togglePlayMock} />
</div>,
);
fireEvent.click(container.getByTitle('firstMedia'));
expect(togglePlayMock).toHaveBeenCalledTimes(1);
currentMedia = container.getByTitle('firstMedia');
expect(currentMedia.textContent).toMatch(/Playing/);
fireEvent.click(container.getByTitle('secondMedia'));
expect(togglePlayMock).toHaveBeenCalledTimes(2);
currentMedia = container.getByTitle('secondMedia');
expect(currentMedia.textContent).toMatch(/Playing/);
});
});

View file

@ -289,7 +289,7 @@ class Header extends ImmutablePureComponent {
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />; lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />;
} }
if (signedIn && account.get('id') !== me) { if (signedIn && account.get('id') !== me && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
menu.push(null); menu.push(null);
@ -299,7 +299,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
} }
if ('share' in navigator) { if ('share' in navigator && !account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
menu.push(null); menu.push(null);
} }
@ -347,7 +347,9 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true }); menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
} }
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); if (!account.get('suspended')) {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
}
} }
if (signedIn && isRemote) { if (signedIn && isRemote) {
@ -395,7 +397,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__image'> <div className='account__header__image'>
<div className='account__header__info'> <div className='account__header__info'>
{!suspended && info} {info}
</div> </div>
{!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />} {!(suspended || hidden) && <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />}
@ -407,18 +409,16 @@ class Header extends ImmutablePureComponent {
<Avatar account={suspended || hidden ? undefined : account} size={90} /> <Avatar account={suspended || hidden ? undefined : account} size={90} />
</a> </a>
{!suspended && ( <div className='account__header__tabs__buttons'>
<div className='account__header__tabs__buttons'> {!hidden && (
{!hidden && ( <>
<> {actionBtn}
{actionBtn} {bellBtn}
{bellBtn} </>
</> )}
)}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
</div> </div>
)}
</div> </div>
<div className='account__header__tabs__name'> <div className='account__header__tabs__name'>

View file

@ -178,7 +178,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
modalType: 'IMAGE', modalType: 'IMAGE',
modalProps: { modalProps: {
src: account.get('avatar'), src: account.get('avatar'),
alt: account.get('acct'), alt: '',
}, },
})); }));
}, },

View file

@ -20,6 +20,7 @@ import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/featur
import { Blurhash } from '../../components/blurhash'; import { Blurhash } from '../../components/blurhash';
import { displayMedia, useBlurhash } from '../../initial_state'; import { displayMedia, useBlurhash } from '../../initial_state';
import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
import Visualizer from './visualizer'; import Visualizer from './visualizer';
@ -165,15 +166,32 @@ class Audio extends PureComponent {
} }
togglePlay = () => { togglePlay = () => {
if (!this.audioContext) { const audios = document.querySelectorAll('audio');
this._initAudioContext();
audios.forEach((audio) => {
const button = audio.previousElementSibling;
button.addEventListener('click', () => {
if(audio.paused) {
audios.forEach((e) => {
if (e !== audio) {
e.pause();
}
});
audio.play();
this.setState({ paused: false });
} else {
audio.pause();
this.setState({ paused: true });
}
});
});
if (currentMedia !== null) {
currentMedia.pause();
} }
if (this.state.paused) { this.audio.play();
this.setState({ paused: false }, () => this.audio.play()); setCurrentMedia(this.audio);
} else {
this.setState({ paused: true }, () => this.audio.pause());
}
}; };
handleResize = debounce(() => { handleResize = debounce(() => {
@ -195,6 +213,7 @@ class Audio extends PureComponent {
}; };
handlePause = () => { handlePause = () => {
this.audio.pause();
this.setState({ paused: true }); this.setState({ paused: true });
if (this.audioContext) { if (this.audioContext) {

View file

@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true), isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
suspended: state.getIn(['accounts', accountId, 'suspended'], false), suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
hidden: getAccountHidden(state, accountId), hidden: getAccountHidden(state, accountId),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false), blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
}; };
@ -111,7 +112,7 @@ class Followers extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props; const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
if (!isAccount) { if (!isAccount) {
return ( return (
@ -137,6 +138,8 @@ class Followers extends ImmutablePureComponent {
emptyMessage = <LimitedAccountHint accountId={accountId} />; emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) { } else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />; emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) { } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint url={remoteUrl} />;
} else { } else {

View file

@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']), hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true), isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
suspended: state.getIn(['accounts', accountId, 'suspended'], false), suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
hidden: getAccountHidden(state, accountId), hidden: getAccountHidden(state, accountId),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false), blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
}; };
@ -111,7 +112,7 @@ class Following extends ImmutablePureComponent {
}, 300, { leading: true }); }, 300, { leading: true });
render () { render () {
const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl } = this.props; const { accountId, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, suspended, hidden, remote, remoteUrl, hideCollections } = this.props;
if (!isAccount) { if (!isAccount) {
return ( return (
@ -137,6 +138,8 @@ class Following extends ImmutablePureComponent {
emptyMessage = <LimitedAccountHint accountId={accountId} />; emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) { } else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />; emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (hideCollections && accountIds.isEmpty()) {
emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
} else if (remote && accountIds.isEmpty()) { } else if (remote && accountIds.isEmpty()) {
emptyMessage = <RemoteHint url={remoteUrl} />; emptyMessage = <RemoteHint url={remoteUrl} />;
} else { } else {

View file

@ -204,7 +204,7 @@ class ListTimeline extends PureComponent {
</div> </div>
<div className='setting-toggle'> <div className='setting-toggle'>
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} /> <Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'> <label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' /> <FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
</label> </label>

View file

@ -18,6 +18,8 @@ import { ReactComponent as ReplyAllIcon } from '@material-symbols/svg-600/outlin
import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/star-fill.svg'; import { ReactComponent as StarIcon } from '@material-symbols/svg-600/outlined/star-fill.svg';
import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg'; import { ReactComponent as StarBorderIcon } from '@material-symbols/svg-600/outlined/star.svg';
import { ReactComponent as RepeatDisabledIcon } from 'mastodon/../svg-icons/repeat_disabled.svg';
import { ReactComponent as RepeatPrivateIcon } from 'mastodon/../svg-icons/repeat_private.svg';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { WithRouterPropTypes } from 'mastodon/utils/react_router';
@ -280,6 +282,7 @@ class ActionBar extends PureComponent {
let replyIcon; let replyIcon;
let replyIconComponent; let replyIconComponent;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply'; replyIcon = 'reply';
replyIconComponent = ReplyIcon; replyIconComponent = ReplyIcon;
@ -290,21 +293,26 @@ class ActionBar extends PureComponent {
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
let reblogTitle; let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) { if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) { } else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog); reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) { } else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private); reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else { } else {
reblogTitle = intl.formatMessage(messages.cannot_reblog); reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
} }
return ( return (
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} /></div> <div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div> <div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={status.get('favourited') ? StarIcon : StarBorderIcon} onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} /></div>

View file

@ -19,8 +19,10 @@ import { throttle } from 'lodash';
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 { playerSettings } from 'mastodon/settings';
import { displayMedia, useBlurhash } from '../../initial_state'; import { displayMedia, useBlurhash } from '../../initial_state';
import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
const messages = defineMessages({ const messages = defineMessages({
@ -180,6 +182,7 @@ class Video extends PureComponent {
}; };
handlePause = () => { handlePause = () => {
this.video.pause();
this.setState({ paused: true }); this.setState({ paused: true });
}; };
@ -226,8 +229,8 @@ class Video extends PureComponent {
if(!isNaN(x)) { if(!isNaN(x)) {
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => { this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
this.video.volume = x; this._syncVideoToVolumeState(x);
this.video.muted = this.state.muted; this._saveVolumeState(x);
}); });
} }
}, 15); }, 15);
@ -343,11 +346,32 @@ class Video extends PureComponent {
}; };
togglePlay = () => { togglePlay = () => {
if (this.state.paused) { const videos = document.querySelectorAll('video');
this.setState({ paused: false }, () => this.video.play());
} else { videos.forEach((video) => {
this.setState({ paused: true }, () => this.video.pause()); const button = video.nextElementSibling;
button.addEventListener('click', () => {
if (video.paused) {
videos.forEach((e) => {
if (e !== video) {
e.pause();
}
});
video.play();
this.setState({ paused: false });
} else {
video.pause();
this.setState({ paused: true });
}
});
});
if (currentMedia !== null) {
currentMedia.pause();
} }
this.video.play();
setCurrentMedia(this.video);
}; };
toggleFullscreen = () => { toggleFullscreen = () => {
@ -365,6 +389,8 @@ class Video extends PureComponent {
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true); document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
window.addEventListener('scroll', this.handleScroll); window.addEventListener('scroll', this.handleScroll);
this._syncVideoFromLocalStorage();
} }
componentWillUnmount () { componentWillUnmount () {
@ -437,8 +463,28 @@ class Video extends PureComponent {
const muted = !(this.video.muted || this.state.volume === 0); const muted = !(this.video.muted || this.state.volume === 0);
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => { this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
this.video.volume = this.state.volume; this._syncVideoToVolumeState();
this.video.muted = this.state.muted; this._saveVolumeState();
});
};
_syncVideoToVolumeState = (volume = null, muted = null) => {
if (!this.video) {
return;
}
this.video.volume = volume ?? this.state.volume;
this.video.muted = muted ?? this.state.muted;
};
_saveVolumeState = (volume = null, muted = null) => {
playerSettings.set('volume', volume ?? this.state.volume);
playerSettings.set('muted', muted ?? this.state.muted);
};
_syncVideoFromLocalStorage = () => {
this.setState({ volume: playerSettings.get('volume') ?? 0.5, muted: playerSettings.get('muted') ?? false }, () => {
this._syncVideoToVolumeState();
}); });
}; };
@ -480,6 +526,7 @@ class Video extends PureComponent {
handleVolumeChange = () => { handleVolumeChange = () => {
this.setState({ volume: this.video.volume, muted: this.video.muted }); this.setState({ volume: this.video.volume, muted: this.video.muted });
this._saveVolumeState(this.video.volume, this.video.muted);
}; };
handleOpenVideo = () => { handleOpenVideo = () => {

View file

@ -14,6 +14,7 @@
"account.badges.group": "Groep", "account.badges.group": "Groep",
"account.block": "Blokkeer @{name}", "account.block": "Blokkeer @{name}",
"account.block_domain": "Blokkeer domein {domain}", "account.block_domain": "Blokkeer domein {domain}",
"account.block_short": "Blokkeer",
"account.blocked": "Geblokkeer", "account.blocked": "Geblokkeer",
"account.browse_more_on_origin_server": "Verken die oorspronklike profiel", "account.browse_more_on_origin_server": "Verken die oorspronklike profiel",
"account.cancel_follow_request": "Herroep volgversoek", "account.cancel_follow_request": "Herroep volgversoek",
@ -45,6 +46,7 @@
"account.posts_with_replies": "Plasings en antwoorde", "account.posts_with_replies": "Plasings en antwoorde",
"account.report": "Rapporteer @{name}", "account.report": "Rapporteer @{name}",
"account.requested": "Wag op goedkeuring. Klik om volgversoek te kanselleer", "account.requested": "Wag op goedkeuring. Klik om volgversoek te kanselleer",
"account.requested_follow": "{name} het versoek om jou te volg",
"account.share": "Deel @{name} se profiel", "account.share": "Deel @{name} se profiel",
"account.show_reblogs": "Wys aangestuurde plasings van @{name}", "account.show_reblogs": "Wys aangestuurde plasings van @{name}",
"account.statuses_counter": "{count, plural, one {{counter} Plaas} other {{counter} Plasings}}", "account.statuses_counter": "{count, plural, one {{counter} Plaas} other {{counter} Plasings}}",
@ -82,6 +84,7 @@
"column.community": "Plaaslike tydlyn", "column.community": "Plaaslike tydlyn",
"column.directory": "Blaai deur profiele", "column.directory": "Blaai deur profiele",
"column.domain_blocks": "Geblokkeerde domeine", "column.domain_blocks": "Geblokkeerde domeine",
"column.favourites": "Gunstelinge",
"column.follow_requests": "Volgversoeke", "column.follow_requests": "Volgversoeke",
"column.home": "Tuis", "column.home": "Tuis",
"column.lists": "Lyste", "column.lists": "Lyste",
@ -271,6 +274,7 @@
"privacy.unlisted.short": "Ongelys", "privacy.unlisted.short": "Ongelys",
"privacy_policy.last_updated": "Laaste bywerking op {date}", "privacy_policy.last_updated": "Laaste bywerking op {date}",
"privacy_policy.title": "Privaatheidsbeleid", "privacy_policy.title": "Privaatheidsbeleid",
"regeneration_indicator.sublabel": "Jou tuis-voer word voorberei!",
"reply_indicator.cancel": "Kanselleer", "reply_indicator.cancel": "Kanselleer",
"report.placeholder": "Type or paste additional comments", "report.placeholder": "Type or paste additional comments",
"report.submit": "Submit report", "report.submit": "Submit report",

View file

@ -201,7 +201,7 @@
"disabled_account_banner.text": "Ваш уліковы запіс {disabledAccount} часова адключаны.", "disabled_account_banner.text": "Ваш уліковы запіс {disabledAccount} часова адключаны.",
"dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.", "dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.",
"dismissable_banner.dismiss": "Адхіліць", "dismissable_banner.dismiss": "Адхіліць",
"dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца прама зараз на гэтым і іншых серверах дэцэнтралізаванай сеткі.", "dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца цяпер на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
"dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.", "dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.",
"dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі", "dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі",
"dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.", "dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.",
@ -222,6 +222,7 @@
"emoji_button.search_results": "Вынікі пошуку", "emoji_button.search_results": "Вынікі пошуку",
"emoji_button.symbols": "Сімвалы", "emoji_button.symbols": "Сімвалы",
"emoji_button.travel": "Падарожжы і месцы", "emoji_button.travel": "Падарожжы і месцы",
"empty_column.account_hides_collections": "Гэты карыстальнік вырашыў схаваць гэтую інфармацыю",
"empty_column.account_suspended": "Уліковы запіс прыпынены", "empty_column.account_suspended": "Уліковы запіс прыпынены",
"empty_column.account_timeline": "Тут няма допісаў!", "empty_column.account_timeline": "Тут няма допісаў!",
"empty_column.account_unavailable": "Профіль недаступны", "empty_column.account_unavailable": "Профіль недаступны",
@ -481,7 +482,7 @@
"onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!", "onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!",
"onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}", "onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}",
"onboarding.share.next_steps": "Магчымыя наступныя крокі:", "onboarding.share.next_steps": "Магчымыя наступныя крокі:",
"onboarding.share.title": "Падзяліцеся сваім профілем", "onboarding.share.title": "Абагульце свой профіль",
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
"onboarding.start.skip": "Want to skip right ahead?", "onboarding.start.skip": "Want to skip right ahead?",
"onboarding.start.title": "Вы зрабілі гэта!", "onboarding.start.title": "Вы зрабілі гэта!",
@ -492,7 +493,7 @@
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.", "onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
"onboarding.steps.setup_profile.title": "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": "Абагульць ваш профіль у Mastodon",
"onboarding.tips.2fa": "<strong>Ці вы ведаеце?</strong> Вы можаце абараніць свой уліковы запіс, усталяваўшы двухфактарную аўтэнтыфікацыю ў наладах уліковага запісу. Яна працуе з любой праграмай TOTP на ваш выбар, нумар тэлефона не патрэбны!", "onboarding.tips.2fa": "<strong>Ці вы ведаеце?</strong> Вы можаце абараніць свой уліковы запіс, усталяваўшы двухфактарную аўтэнтыфікацыю ў наладах уліковага запісу. Яна працуе з любой праграмай TOTP на ваш выбар, нумар тэлефона не патрэбны!",
"onboarding.tips.accounts_from_other_servers": "<strong>Ці вы ведаеце?</strong> Паколькі Mastodon дэцэнтралізаваны, некаторыя профілі, якія вам трапляюцца, будуць размяшчацца на іншых серверах, адрозных ад вашага. І ўсё ж вы можаце бесперашкодна ўзаемадзейнічаць з імі! Іх сервер пазначаны ў другой палове імя карыстальніка!", "onboarding.tips.accounts_from_other_servers": "<strong>Ці вы ведаеце?</strong> Паколькі Mastodon дэцэнтралізаваны, некаторыя профілі, якія вам трапляюцца, будуць размяшчацца на іншых серверах, адрозных ад вашага. І ўсё ж вы можаце бесперашкодна ўзаемадзейнічаць з імі! Іх сервер пазначаны ў другой палове імя карыстальніка!",
"onboarding.tips.migration": "<strong>Ці вы ведаеце?</strong> Калі вы адчуваеце, што {domain} не з'яўляецца для вас лепшым выбарам у будучыні, вы можаце перайсці на іншы сервер Mastodon, не губляючы сваіх падпісчыкаў. Вы нават можаце стварыць свой уласны сервер!", "onboarding.tips.migration": "<strong>Ці вы ведаеце?</strong> Калі вы адчуваеце, што {domain} не з'яўляецца для вас лепшым выбарам у будучыні, вы можаце перайсці на іншы сервер Mastodon, не губляючы сваіх падпісчыкаў. Вы нават можаце стварыць свой уласны сервер!",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Resultats de la cerca", "emoji_button.search_results": "Resultats de la cerca",
"emoji_button.symbols": "Símbols", "emoji_button.symbols": "Símbols",
"emoji_button.travel": "Viatges i llocs", "emoji_button.travel": "Viatges i llocs",
"empty_column.account_hides_collections": "Aquest usuari ha elegit no mostrar aquesta informació",
"empty_column.account_suspended": "Compte suspès", "empty_column.account_suspended": "Compte suspès",
"empty_column.account_timeline": "No hi ha tuts aquí!", "empty_column.account_timeline": "No hi ha tuts aquí!",
"empty_column.account_unavailable": "Perfil no disponible", "empty_column.account_unavailable": "Perfil no disponible",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Canlyniadau chwilio", "emoji_button.search_results": "Canlyniadau chwilio",
"emoji_button.symbols": "Symbolau", "emoji_button.symbols": "Symbolau",
"emoji_button.travel": "Teithio a Llefydd", "emoji_button.travel": "Teithio a Llefydd",
"empty_column.account_hides_collections": "Mae'r defnyddiwr wedi dewis i beidio rhannu'r wybodaeth yma",
"empty_column.account_suspended": "Cyfrif wedi'i atal", "empty_column.account_suspended": "Cyfrif wedi'i atal",
"empty_column.account_timeline": "Dim postiadau yma!", "empty_column.account_timeline": "Dim postiadau yma!",
"empty_column.account_unavailable": "Nid yw'r proffil ar gael", "empty_column.account_unavailable": "Nid yw'r proffil ar gael",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Søgeresultater", "emoji_button.search_results": "Søgeresultater",
"emoji_button.symbols": "Symboler", "emoji_button.symbols": "Symboler",
"emoji_button.travel": "Rejser og steder", "emoji_button.travel": "Rejser og steder",
"empty_column.account_hides_collections": "Brugeren har valgt ikke at gøre denne information tilgængelig",
"empty_column.account_suspended": "Konto suspenderet", "empty_column.account_suspended": "Konto suspenderet",
"empty_column.account_timeline": "Ingen indlæg hér!", "empty_column.account_timeline": "Ingen indlæg hér!",
"empty_column.account_unavailable": "Profil utilgængelig", "empty_column.account_unavailable": "Profil utilgængelig",

View file

@ -88,7 +88,7 @@
"attachments_list.unprocessed": "(ausstehend)", "attachments_list.unprocessed": "(ausstehend)",
"audio.hide": "Audio ausblenden", "audio.hide": "Audio ausblenden",
"autosuggest_hashtag.per_week": "{count} pro Woche", "autosuggest_hashtag.per_week": "{count} pro Woche",
"boost_modal.combo": "Drücke {combo}, um das beim nächsten Mal zu überspringen", "boost_modal.combo": "Mit {combo} wird dieses Fenster beim nächsten Mal nicht mehr angezeigt",
"bundle_column_error.copy_stacktrace": "Fehlerbericht kopieren", "bundle_column_error.copy_stacktrace": "Fehlerbericht kopieren",
"bundle_column_error.error.body": "Die angeforderte Seite konnte nicht dargestellt werden. Dies könnte auf einen Fehler in unserem Code oder auf ein Browser-Kompatibilitätsproblem zurückzuführen sein.", "bundle_column_error.error.body": "Die angeforderte Seite konnte nicht dargestellt werden. Dies könnte auf einen Fehler in unserem Code oder auf ein Browser-Kompatibilitätsproblem zurückzuführen sein.",
"bundle_column_error.error.title": "Oh nein!", "bundle_column_error.error.title": "Oh nein!",
@ -222,6 +222,7 @@
"emoji_button.search_results": "Suchergebnisse", "emoji_button.search_results": "Suchergebnisse",
"emoji_button.symbols": "Symbole", "emoji_button.symbols": "Symbole",
"emoji_button.travel": "Reisen & Orte", "emoji_button.travel": "Reisen & Orte",
"empty_column.account_hides_collections": "Das Konto hat sich dazu entschieden, diese Information nicht zu veröffentlichen",
"empty_column.account_suspended": "Konto gesperrt", "empty_column.account_suspended": "Konto gesperrt",
"empty_column.account_timeline": "Keine Beiträge vorhanden!", "empty_column.account_timeline": "Keine Beiträge vorhanden!",
"empty_column.account_unavailable": "Profil nicht verfügbar", "empty_column.account_unavailable": "Profil nicht verfügbar",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Search results", "emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols", "emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places", "emoji_button.travel": "Travel & Places",
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
"empty_column.account_suspended": "Account suspended", "empty_column.account_suspended": "Account suspended",
"empty_column.account_timeline": "No posts here!", "empty_column.account_timeline": "No posts here!",
"empty_column.account_unavailable": "Profile unavailable", "empty_column.account_unavailable": "Profile unavailable",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Resultados de búsqueda", "emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares", "emoji_button.travel": "Viajes y lugares",
"empty_column.account_hides_collections": "Este usuario eligió no publicar esta información",
"empty_column.account_suspended": "Cuenta suspendida", "empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay mensajes acá!", "empty_column.account_timeline": "¡No hay mensajes acá!",
"empty_column.account_unavailable": "Perfil no disponible", "empty_column.account_unavailable": "Perfil no disponible",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Resultados de búsqueda", "emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares", "emoji_button.travel": "Viajes y lugares",
"empty_column.account_hides_collections": "Este usuario ha elegido no hacer disponible esta información",
"empty_column.account_suspended": "Cuenta suspendida", "empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay toots aquí!", "empty_column.account_timeline": "¡No hay toots aquí!",
"empty_column.account_unavailable": "Perfil no disponible", "empty_column.account_unavailable": "Perfil no disponible",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Resultados de búsqueda", "emoji_button.search_results": "Resultados de búsqueda",
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viajes y lugares", "emoji_button.travel": "Viajes y lugares",
"empty_column.account_hides_collections": "Este usuario ha decidido no mostrar esta información",
"empty_column.account_suspended": "Cuenta suspendida", "empty_column.account_suspended": "Cuenta suspendida",
"empty_column.account_timeline": "¡No hay publicaciones aquí!", "empty_column.account_timeline": "¡No hay publicaciones aquí!",
"empty_column.account_unavailable": "Perfil no disponible", "empty_column.account_unavailable": "Perfil no disponible",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Otsitulemused", "emoji_button.search_results": "Otsitulemused",
"emoji_button.symbols": "Sümbolid", "emoji_button.symbols": "Sümbolid",
"emoji_button.travel": "Reisimine & kohad", "emoji_button.travel": "Reisimine & kohad",
"empty_column.account_hides_collections": "See kasutaja otsustas mitte teha seda infot saadavaks",
"empty_column.account_suspended": "Konto kustutatud", "empty_column.account_suspended": "Konto kustutatud",
"empty_column.account_timeline": "Siin postitusi ei ole!", "empty_column.account_timeline": "Siin postitusi ei ole!",
"empty_column.account_unavailable": "Profiil pole saadaval", "empty_column.account_unavailable": "Profiil pole saadaval",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Bilaketaren emaitzak", "emoji_button.search_results": "Bilaketaren emaitzak",
"emoji_button.symbols": "Sinboloak", "emoji_button.symbols": "Sinboloak",
"emoji_button.travel": "Bidaiak eta tokiak", "emoji_button.travel": "Bidaiak eta tokiak",
"empty_column.account_hides_collections": "Erabiltzaile honek informazio hau erabilgarri ez egotea aukeratu du.",
"empty_column.account_suspended": "Kanporatutako kontua", "empty_column.account_suspended": "Kanporatutako kontua",
"empty_column.account_timeline": "Ez dago bidalketarik hemen!", "empty_column.account_timeline": "Ez dago bidalketarik hemen!",
"empty_column.account_unavailable": "Profila ez dago eskuragarri", "empty_column.account_unavailable": "Profila ez dago eskuragarri",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "نتایج جست‌وجو", "emoji_button.search_results": "نتایج جست‌وجو",
"emoji_button.symbols": "نمادها", "emoji_button.symbols": "نمادها",
"emoji_button.travel": "سفر و مکان", "emoji_button.travel": "سفر و مکان",
"empty_column.account_hides_collections": "کاربر خواسته که این اطّلاعات در دسترس نباشند",
"empty_column.account_suspended": "حساب معلق شد", "empty_column.account_suspended": "حساب معلق شد",
"empty_column.account_timeline": "هیچ فرسته‌ای این‌جا نیست!", "empty_column.account_timeline": "هیچ فرسته‌ای این‌جا نیست!",
"empty_column.account_unavailable": "نمایهٔ موجود نیست", "empty_column.account_unavailable": "نمایهٔ موجود نیست",
@ -358,13 +359,13 @@
"keyboard_shortcuts.profile": "گشودن نمایهٔ نویسنده", "keyboard_shortcuts.profile": "گشودن نمایهٔ نویسنده",
"keyboard_shortcuts.reply": "پاسخ به فرسته", "keyboard_shortcuts.reply": "پاسخ به فرسته",
"keyboard_shortcuts.requests": "گشودن سیاههٔ درخواست‌های پی‌گیری", "keyboard_shortcuts.requests": "گشودن سیاههٔ درخواست‌های پی‌گیری",
"keyboard_shortcuts.search": "تمرکز روی جست‌وجو", "keyboard_shortcuts.search": "تمرکز روی نوار جست‌وجو",
"keyboard_shortcuts.spoilers": "نمایش/نهفتن زمینهٔ هشدار محتوا", "keyboard_shortcuts.spoilers": "نمایش/نهفتن زمینهٔ هشدار محتوا",
"keyboard_shortcuts.start": "گشودن ستون «آغاز کنید»", "keyboard_shortcuts.start": "گشودن ستون «آغاز کنید»",
"keyboard_shortcuts.toggle_hidden": "نمایش/نهفتن نوشتهٔ پشت هشدار محتوا", "keyboard_shortcuts.toggle_hidden": "نمایش/نهفتن نوشتهٔ پشت هشدار محتوا",
"keyboard_shortcuts.toggle_sensitivity": "نمایش/نهفتن رسانه", "keyboard_shortcuts.toggle_sensitivity": "نمایش/نهفتن رسانه",
"keyboard_shortcuts.toot": "شروع یک فرستهٔ جدید", "keyboard_shortcuts.toot": "شروع یک فرستهٔ جدید",
"keyboard_shortcuts.unfocus": "برداشتن تمرکز از نوشتن/جست‌وجو", "keyboard_shortcuts.unfocus": "برداشتن تمرکز از ناحیهٔ نوشتن یا جست‌وجو",
"keyboard_shortcuts.up": "بالا بردن در سیاهه", "keyboard_shortcuts.up": "بالا بردن در سیاهه",
"lightbox.close": "بستن", "lightbox.close": "بستن",
"lightbox.compress": "فشرده‌سازی جعبهٔ نمایش تصویر", "lightbox.compress": "فشرده‌سازی جعبهٔ نمایش تصویر",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Hakutulokset", "emoji_button.search_results": "Hakutulokset",
"emoji_button.symbols": "Symbolit", "emoji_button.symbols": "Symbolit",
"emoji_button.travel": "Matkailu ja paikat", "emoji_button.travel": "Matkailu ja paikat",
"empty_column.account_hides_collections": "Käyttäjä on päättänyt olla julkaisematta näitä tietoja",
"empty_column.account_suspended": "Tili jäädytetty", "empty_column.account_suspended": "Tili jäädytetty",
"empty_column.account_timeline": "Ei viestejä täällä.", "empty_column.account_timeline": "Ei viestejä täällä.",
"empty_column.account_unavailable": "Profiilia ei löydy", "empty_column.account_unavailable": "Profiilia ei löydy",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Leitiúrslit", "emoji_button.search_results": "Leitiúrslit",
"emoji_button.symbols": "Ímyndir", "emoji_button.symbols": "Ímyndir",
"emoji_button.travel": "Ferðing og støð", "emoji_button.travel": "Ferðing og støð",
"empty_column.account_hides_collections": "Hesin brúkarin hevur valt, at hesar upplýsingarnar ikki skulu vera tøkar",
"empty_column.account_suspended": "Kontan gjørd óvirkin", "empty_column.account_suspended": "Kontan gjørd óvirkin",
"empty_column.account_timeline": "Einki uppslag her!", "empty_column.account_timeline": "Einki uppslag her!",
"empty_column.account_unavailable": "Vangin er ikki tøkur", "empty_column.account_unavailable": "Vangin er ikki tøkur",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Résultats", "emoji_button.search_results": "Résultats",
"emoji_button.symbols": "Symboles", "emoji_button.symbols": "Symboles",
"emoji_button.travel": "Voyage et lieux", "emoji_button.travel": "Voyage et lieux",
"empty_column.account_hides_collections": "Cet utilisateur·ice préfère ne pas rendre publiques ces informations",
"empty_column.account_suspended": "Compte suspendu", "empty_column.account_suspended": "Compte suspendu",
"empty_column.account_timeline": "Aucune publication ici!", "empty_column.account_timeline": "Aucune publication ici!",
"empty_column.account_unavailable": "Profil non disponible", "empty_column.account_unavailable": "Profil non disponible",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Résultats de la recherche", "emoji_button.search_results": "Résultats de la recherche",
"emoji_button.symbols": "Symboles", "emoji_button.symbols": "Symboles",
"emoji_button.travel": "Voyage et lieux", "emoji_button.travel": "Voyage et lieux",
"empty_column.account_hides_collections": "Cet utilisateur·ice préfère ne pas rendre publiques ces informations",
"empty_column.account_suspended": "Compte suspendu", "empty_column.account_suspended": "Compte suspendu",
"empty_column.account_timeline": "Aucun message ici !", "empty_column.account_timeline": "Aucun message ici !",
"empty_column.account_unavailable": "Profil non disponible", "empty_column.account_unavailable": "Profil non disponible",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Sykresultaten", "emoji_button.search_results": "Sykresultaten",
"emoji_button.symbols": "Symboalen", "emoji_button.symbols": "Symboalen",
"emoji_button.travel": "Reizgje en lokaasjes", "emoji_button.travel": "Reizgje en lokaasjes",
"empty_column.account_hides_collections": "Dizze brûker hat derfoar keazen dizze ynformaasje net beskikber te meitsjen",
"empty_column.account_suspended": "Account beskoattele", "empty_column.account_suspended": "Account beskoattele",
"empty_column.account_timeline": "Hjir binne gjin berjochten!", "empty_column.account_timeline": "Hjir binne gjin berjochten!",
"empty_column.account_unavailable": "Profyl net beskikber", "empty_column.account_unavailable": "Profyl net beskikber",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Resultados da procura", "emoji_button.search_results": "Resultados da procura",
"emoji_button.symbols": "Símbolos", "emoji_button.symbols": "Símbolos",
"emoji_button.travel": "Viaxes e Lugares", "emoji_button.travel": "Viaxes e Lugares",
"empty_column.account_hides_collections": "A usuaria decideu non facer pública esta información",
"empty_column.account_suspended": "Conta suspendida", "empty_column.account_suspended": "Conta suspendida",
"empty_column.account_timeline": "Non hai publicacións aquí!", "empty_column.account_timeline": "Non hai publicacións aquí!",
"empty_column.account_unavailable": "Perfil non dispoñible", "empty_column.account_unavailable": "Perfil non dispoñible",

View file

@ -62,7 +62,7 @@
"account.share": "שתף את הפרופיל של @{name}", "account.share": "שתף את הפרופיל של @{name}",
"account.show_reblogs": "הצג הדהודים מאת @{name}", "account.show_reblogs": "הצג הדהודים מאת @{name}",
"account.statuses_counter": "{count, plural, one {הודעה} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}", "account.statuses_counter": "{count, plural, one {הודעה} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}",
"account.unblock": "הסר את החסימה של @{name}", "account.unblock": "להסיר חסימה ל- @{name}",
"account.unblock_domain": "הסירי את החסימה של קהילת {domain}", "account.unblock_domain": "הסירי את החסימה של קהילת {domain}",
"account.unblock_short": "הסר חסימה", "account.unblock_short": "הסר חסימה",
"account.unendorse": "אל תקדם בפרופיל", "account.unendorse": "אל תקדם בפרופיל",
@ -222,6 +222,7 @@
"emoji_button.search_results": "תוצאות חיפוש", "emoji_button.search_results": "תוצאות חיפוש",
"emoji_button.symbols": "סמלים", "emoji_button.symbols": "סמלים",
"emoji_button.travel": "טיולים ואתרים", "emoji_button.travel": "טיולים ואתרים",
"empty_column.account_hides_collections": "המשתמש.ת בחר.ה להסתיר מידע זה",
"empty_column.account_suspended": "חשבון מושהה", "empty_column.account_suspended": "חשבון מושהה",
"empty_column.account_timeline": "אין עדיין אף הודעה!", "empty_column.account_timeline": "אין עדיין אף הודעה!",
"empty_column.account_unavailable": "פרופיל לא זמין", "empty_column.account_unavailable": "פרופיל לא זמין",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Keresési találatok", "emoji_button.search_results": "Keresési találatok",
"emoji_button.symbols": "Szimbólumok", "emoji_button.symbols": "Szimbólumok",
"emoji_button.travel": "Utazás és Helyek", "emoji_button.travel": "Utazás és Helyek",
"empty_column.account_hides_collections": "Ez a felhasználó úgy döntött, hogy nem teszi elérhetővé ezt az információt.",
"empty_column.account_suspended": "Fiók felfüggesztve", "empty_column.account_suspended": "Fiók felfüggesztve",
"empty_column.account_timeline": "Itt nincs bejegyzés!", "empty_column.account_timeline": "Itt nincs bejegyzés!",
"empty_column.account_unavailable": "A profil nem érhető el", "empty_column.account_unavailable": "A profil nem érhető el",

View file

@ -1,8 +1,11 @@
{ {
"account.add_or_remove_from_list": "Tinye ma ọ bụ Wepu na ndepụta", "account.add_or_remove_from_list": "Tinye ma ọ bụ Wepu na ndepụta",
"account.badges.bot": "Bot", "account.badges.bot": "Bot",
"account.badges.group": "Otù",
"account.cancel_follow_request": "Withdraw follow request", "account.cancel_follow_request": "Withdraw follow request",
"account.follow": "Soro", "account.follow": "Soro",
"account.followers": "Ndị na-eso",
"account.following": "Na-eso",
"account.follows_you": "Na-eso gị", "account.follows_you": "Na-eso gị",
"account.mute": "Mee ogbi @{name}", "account.mute": "Mee ogbi @{name}",
"account.unfollow": "Kwụsị iso", "account.unfollow": "Kwụsị iso",
@ -11,16 +14,20 @@
"audio.hide": "Zoo ụda", "audio.hide": "Zoo ụda",
"bundle_column_error.retry": "Nwaa ọzọ", "bundle_column_error.retry": "Nwaa ọzọ",
"bundle_column_error.routing.title": "404", "bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Mechie",
"bundle_modal_error.retry": "Nwaa ọzọ", "bundle_modal_error.retry": "Nwaa ọzọ",
"column.about": "Maka", "column.about": "Maka",
"column.blocks": "Ojiarụ egbochiri", "column.blocks": "Ojiarụ egbochiri",
"column.bookmarks": "Ebenrụtụakā", "column.bookmarks": "Ebenrụtụakā",
"column.home": "Be", "column.home": "Be",
"column.lists": "Ndepụta",
"column.pins": "Pinned post", "column.pins": "Pinned post",
"column_header.pin": "Gbado na profaịlụ gị",
"column_subheading.settings": "Mwube", "column_subheading.settings": "Mwube",
"community.column_settings.media_only": "Media only", "community.column_settings.media_only": "Media only",
"compose.language.change": "Gbanwee asụsụ", "compose.language.change": "Gbanwee asụsụ",
"compose.language.search": "Chọọ asụsụ...", "compose.language.search": "Chọọ asụsụ...",
"compose.published.open": "Mepe",
"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.placeholder": "What is on your mind?", "compose_form.placeholder": "What is on your mind?",
@ -32,7 +39,10 @@
"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": "Hichapụ", "confirmations.delete_list.confirm": "Hichapụ",
"confirmations.domain_block.confirm": "Hide entire domain", "confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.edit.confirm": "Dezie",
"confirmations.mute.confirm": "Mee ogbi",
"confirmations.reply.confirm": "Zaa", "confirmations.reply.confirm": "Zaa",
"confirmations.unfollow.confirm": "Kwụsị iso",
"conversation.delete": "Hichapụ nkata", "conversation.delete": "Hichapụ nkata",
"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.",
@ -76,6 +86,7 @@
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search", "keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "to move up in the list", "keyboard_shortcuts.up": "to move up in the list",
"lists.delete": "Hichapụ ndepụta", "lists.delete": "Hichapụ ndepụta",
"lists.edit": "Dezie ndepụta",
"lists.subheading": "Ndepụta gị", "lists.subheading": "Ndepụta gị",
"loading_indicator.label": "Na-adọnye...", "loading_indicator.label": "Na-adọnye...",
"navigation_bar.about": "Maka", "navigation_bar.about": "Maka",
@ -100,20 +111,27 @@
"privacy.change": "Adjust status privacy", "privacy.change": "Adjust status privacy",
"privacy.direct.short": "Direct", "privacy.direct.short": "Direct",
"privacy.private.short": "Followers-only", "privacy.private.short": "Followers-only",
"relative_time.full.just_now": "kịta",
"relative_time.just_now": "kịta", "relative_time.just_now": "kịta",
"relative_time.today": "taa", "relative_time.today": "taa",
"reply_indicator.cancel": "Kagbuo", "reply_indicator.cancel": "Kagbuo",
"report.categories.other": "Ọzọ", "report.categories.other": "Ọzọ",
"report.categories.spam": "Nzipụ Ozièlètrọniìk Nkeāchọghị",
"report.mute": "Mee ogbi",
"report.placeholder": "Type or paste additional comments", "report.placeholder": "Type or paste additional comments",
"report.submit": "Submit report", "report.submit": "Submit report",
"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 {# post} other {# posts}} attached",
"report_notification.categories.other": "Ọzọ",
"search.placeholder": "Chọọ",
"server_banner.active_users": "ojiarụ dị ìrè", "server_banner.active_users": "ojiarụ dị ìrè",
"server_banner.learn_more": "Mụtakwuo",
"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",
"status.bookmark": "Kee ebenrụtụakā", "status.bookmark": "Kee ebenrụtụakā",
"status.copy": "Copy link to status", "status.copy": "Copy link to status",
"status.delete": "Hichapụ", "status.delete": "Hichapụ",
"status.edit": "Dezie",
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
"status.open": "Expand this status", "status.open": "Expand this status",
"status.remove_bookmark": "Wepu ebenrụtụakā", "status.remove_bookmark": "Wepu ebenrụtụakā",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Leitarniðurstöður", "emoji_button.search_results": "Leitarniðurstöður",
"emoji_button.symbols": "Tákn", "emoji_button.symbols": "Tákn",
"emoji_button.travel": "Ferðalög og staðir", "emoji_button.travel": "Ferðalög og staðir",
"empty_column.account_hides_collections": "Notandinn hefur valið að gera ekki tiltækar þessar upplýsingar",
"empty_column.account_suspended": "Notandaaðgangur í frysti", "empty_column.account_suspended": "Notandaaðgangur í frysti",
"empty_column.account_timeline": "Engar færslur hér!", "empty_column.account_timeline": "Engar færslur hér!",
"empty_column.account_unavailable": "Notandasnið ekki tiltækt", "empty_column.account_unavailable": "Notandasnið ekki tiltækt",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "Risultati della ricerca", "emoji_button.search_results": "Risultati della ricerca",
"emoji_button.symbols": "Simboli", "emoji_button.symbols": "Simboli",
"emoji_button.travel": "Viaggi & Luoghi", "emoji_button.travel": "Viaggi & Luoghi",
"empty_column.account_hides_collections": "Questo utente ha scelto di non rendere disponibili queste informazioni",
"empty_column.account_suspended": "Profilo sospeso", "empty_column.account_suspended": "Profilo sospeso",
"empty_column.account_timeline": "Nessun post qui!", "empty_column.account_timeline": "Nessun post qui!",
"empty_column.account_unavailable": "Profilo non disponibile", "empty_column.account_unavailable": "Profilo non disponibile",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "検索結果", "emoji_button.search_results": "検索結果",
"emoji_button.symbols": "記号", "emoji_button.symbols": "記号",
"emoji_button.travel": "旅行と場所", "emoji_button.travel": "旅行と場所",
"empty_column.account_hides_collections": "このユーザーはこの情報を開示しないことにしています。",
"empty_column.account_suspended": "アカウントは停止されています", "empty_column.account_suspended": "アカウントは停止されています",
"empty_column.account_timeline": "投稿がありません!", "empty_column.account_timeline": "投稿がありません!",
"empty_column.account_unavailable": "プロフィールは利用できません", "empty_column.account_unavailable": "プロフィールは利用できません",
@ -585,8 +586,8 @@
"search.no_recent_searches": "検索履歴はありません", "search.no_recent_searches": "検索履歴はありません",
"search.placeholder": "検索", "search.placeholder": "検索",
"search.quick_action.account_search": "{x}に該当するプロフィール", "search.quick_action.account_search": "{x}に該当するプロフィール",
"search.quick_action.go_to_account": "{x}のプロフィールを見る", "search.quick_action.go_to_account": "プロフィール {x} を見る",
"search.quick_action.go_to_hashtag": "{x}に該当するハッシュタグ", "search.quick_action.go_to_hashtag": "ハッシュタグ {x} を見る",
"search.quick_action.open_url": "MastodonでURLを開く", "search.quick_action.open_url": "MastodonでURLを開く",
"search.quick_action.status_search": "{x}に該当する投稿", "search.quick_action.status_search": "{x}に該当する投稿",
"search.search_or_paste": "検索またはURLを入力", "search.search_or_paste": "検索またはURLを入力",

View file

@ -222,6 +222,7 @@
"emoji_button.search_results": "검색 결과", "emoji_button.search_results": "검색 결과",
"emoji_button.symbols": "기호", "emoji_button.symbols": "기호",
"emoji_button.travel": "여행과 장소", "emoji_button.travel": "여행과 장소",
"empty_column.account_hides_collections": "이 사용자는 이 정보를 사용할 수 없도록 설정했습니다",
"empty_column.account_suspended": "계정 정지됨", "empty_column.account_suspended": "계정 정지됨",
"empty_column.account_timeline": "이곳에는 게시물이 없습니다!", "empty_column.account_timeline": "이곳에는 게시물이 없습니다!",
"empty_column.account_unavailable": "프로필 사용 불가", "empty_column.account_unavailable": "프로필 사용 불가",

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