Merge pull request #2522 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to b2c5b20ef2
This commit is contained in:
commit
4fdbffc57c
@ -4,7 +4,7 @@ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
|
||||
# Install Rails
|
||||
# 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"
|
||||
|
||||
# [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
|
||||
|
||||
# [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
|
||||
|
@ -11,7 +11,8 @@ bundle install
|
||||
git checkout -- Gemfile.lock
|
||||
|
||||
# Fetch Javascript dependencies
|
||||
yarn --frozen-lockfile
|
||||
corepack prepare
|
||||
yarn install --immutable
|
||||
|
||||
# [re]create, migrate, and seed the test database
|
||||
RAILS_ENV=test ./bin/rails db:setup
|
||||
|
27
.github/actions/setup-javascript/action.yml
vendored
27
.github/actions/setup-javascript/action.yml
vendored
@ -11,9 +11,32 @@ runs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
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
|
||||
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'
|
||||
|
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@ -12,6 +12,7 @@
|
||||
// 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.
|
||||
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: [
|
||||
{
|
||||
// Require Dependency Dashboard Approval for major version bumps of these node packages
|
||||
|
9
.gitignore
vendored
9
.gitignore
vendored
@ -55,6 +55,15 @@ npm-debug.log
|
||||
yarn-error.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
|
||||
*-cloudimg-console.log
|
||||
|
||||
|
@ -31,4 +31,3 @@ linters:
|
||||
- 'app/views/admin/accounts/_buttons.html.haml'
|
||||
- 'app/views/admin/accounts/_local_account.html.haml'
|
||||
- 'app/views/admin/roles/_form.html.haml'
|
||||
- 'app/views/layouts/application.html.haml'
|
||||
|
@ -24,15 +24,6 @@ Lint/NonLocalExitFromIterator:
|
||||
Exclude:
|
||||
- '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.
|
||||
Metrics/AbcSize:
|
||||
Max: 144
|
||||
@ -52,48 +43,10 @@ Metrics/CyclomaticComplexity:
|
||||
Metrics/PerceivedComplexity:
|
||||
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.
|
||||
RSpec/ExampleLength:
|
||||
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:
|
||||
Exclude:
|
||||
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
|
||||
@ -136,12 +89,6 @@ RSpec/LetSetup:
|
||||
- 'spec/services/unsuspend_account_service_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:
|
||||
Max: 8
|
||||
|
||||
@ -181,11 +128,6 @@ Rails/HasManyOrHasOneDependent:
|
||||
- 'app/models/user.rb'
|
||||
- 'app/models/web/push_subscription.rb'
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Exclude:
|
||||
- 'lib/tasks/mastodon.rake'
|
||||
- 'spec/helpers/flashes_helper_spec.rb'
|
||||
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/controllers/**/*.rb, app/mailers/**/*.rb
|
||||
Rails/LexicallyScopedActionFilter:
|
||||
@ -561,14 +503,6 @@ Style/SingleArgumentDig:
|
||||
Exclude:
|
||||
- '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).
|
||||
Style/StderrPuts:
|
||||
Exclude:
|
||||
@ -627,5 +561,3 @@ Style/TrailingCommaInHashLiteral:
|
||||
Style/WordArray:
|
||||
Exclude:
|
||||
- 'app/helpers/languages_helper.rb'
|
||||
- 'spec/controllers/settings/imports_controller_spec.rb'
|
||||
- 'spec/models/form/import_spec.rb'
|
||||
|
3
.watchmanconfig
Normal file
3
.watchmanconfig
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"ignore_dirs": ["node_modules/", "public/"]
|
||||
}
|
0
.yarn/.gitkeep
Normal file
0
.yarn/.gitkeep
Normal file
13
.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch
Normal file
13
.yarn/patches/babel-plugin-lodash-npm-3.3.4-c7161075b6.patch
Normal 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;
|
||||
}
|
@ -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)({
|
49
.yarnclean
49
.yarnclean
@ -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
1
.yarnrc.yml
Normal file
@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
14
Dockerfile
14
Dockerfile
@ -13,7 +13,6 @@ ENV DEBIAN_FRONTEND="noninteractive" \
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
WORKDIR /opt/mastodon
|
||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && \
|
||||
@ -36,8 +35,14 @@ RUN apt-get update && \
|
||||
bundle config set --local deployment 'true' && \
|
||||
bundle config set --local without 'development test' && \
|
||||
bundle config set silence_root_warning true && \
|
||||
bundle install -j"$(nproc)" && \
|
||||
yarn install --pure-lockfile --production --network-timeout 600000 && \
|
||||
corepack enable
|
||||
|
||||
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
|
||||
|
||||
FROM node:${NODE_VERSION}
|
||||
@ -78,7 +83,8 @@ RUN apt-get update && \
|
||||
tzdata \
|
||||
libreadline8 \
|
||||
tini && \
|
||||
ln -s /opt/mastodon /mastodon
|
||||
ln -s /opt/mastodon /mastodon && \
|
||||
corepack enable
|
||||
|
||||
# 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
|
||||
|
6
Gemfile
6
Gemfile
@ -16,7 +16,7 @@ gem 'dotenv-rails', '~> 2.8'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||
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 'md-paperclip-azure', '~> 2.2', require: false
|
||||
gem 'blurhash', '~> 0.1'
|
||||
@ -88,7 +88,7 @@ gem 'simple-navigation', '~> 4.4'
|
||||
gem 'simple_form', '~> 5.2'
|
||||
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
||||
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 'twitter-text', '~> 3.1.0'
|
||||
gem 'tzinfo-data', '~> 1.2023'
|
||||
@ -195,7 +195,7 @@ gem 'xorcist', '~> 1.1'
|
||||
|
||||
gem 'cocoon', '~> 1.2'
|
||||
|
||||
gem 'net-http', '~> 0.3.2'
|
||||
gem 'net-http', '~> 0.4.0'
|
||||
gem 'rubyzip', '~> 2.3'
|
||||
|
||||
gem 'hcaptcha', '~> 7.1'
|
||||
|
175
Gemfile.lock
175
Gemfile.lock
@ -39,50 +39,51 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
actioncable (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activejob (= 7.1.1)
|
||||
activerecord (= 7.1.1)
|
||||
activestorage (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
actionmailbox (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activejob (= 7.1.2)
|
||||
activerecord (= 7.1.2)
|
||||
activestorage (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
actionview (= 7.1.1)
|
||||
activejob (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
actionmailer (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
actionview (= 7.1.2)
|
||||
activejob (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.1)
|
||||
actionview (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
actionpack (7.1.2)
|
||||
actionview (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activerecord (= 7.1.1)
|
||||
activestorage (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
actiontext (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activerecord (= 7.1.2)
|
||||
activestorage (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
actionview (7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
@ -92,22 +93,22 @@ GEM
|
||||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
activejob (7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
activerecord (7.1.1)
|
||||
activemodel (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
activemodel (7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
activerecord (7.1.2)
|
||||
activemodel (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activejob (= 7.1.1)
|
||||
activerecord (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
activestorage (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activejob (= 7.1.2)
|
||||
activerecord (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.1.1)
|
||||
activesupport (7.1.2)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
@ -130,8 +131,8 @@ GEM
|
||||
attr_required (1.0.1)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.809.0)
|
||||
aws-sdk-core (3.181.0)
|
||||
aws-partitions (1.828.0)
|
||||
aws-sdk-core (3.183.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@ -139,7 +140,7 @@ GEM
|
||||
aws-sdk-kms (1.71.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
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-kms (~> 1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
@ -218,7 +219,7 @@ GEM
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.3.3)
|
||||
date (3.3.4)
|
||||
debug_inspector (1.1.0)
|
||||
devise (4.9.3)
|
||||
bcrypt (~> 3.0)
|
||||
@ -263,7 +264,7 @@ GEM
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.7)
|
||||
tzinfo
|
||||
excon (0.100.0)
|
||||
excon (0.104.0)
|
||||
fabrication (2.30.0)
|
||||
faker (3.2.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
@ -298,19 +299,18 @@ GEM
|
||||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
rake
|
||||
fog-core (2.1.0)
|
||||
fog-core (2.3.0)
|
||||
builder
|
||||
excon (~> 0.58)
|
||||
formatador (~> 0.2)
|
||||
excon (~> 0.71)
|
||||
formatador (>= 0.2, < 2.0)
|
||||
mime-types
|
||||
fog-json (1.2.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
fog-openstack (0.3.10)
|
||||
fog-core (>= 1.45, <= 2.1.0)
|
||||
fog-openstack (1.1.0)
|
||||
fog-core (~> 2.1)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
formatador (0.3.0)
|
||||
formatador (1.1.0)
|
||||
fugit (1.8.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
@ -370,8 +370,7 @@ GEM
|
||||
terminal-table (>= 1.5.1)
|
||||
idn-ruby (0.1.5)
|
||||
io-console (0.6.0)
|
||||
ipaddress (0.8.3)
|
||||
irb (1.8.1)
|
||||
irb (1.8.3)
|
||||
rdoc
|
||||
reline (>= 0.3.8)
|
||||
jmespath (1.6.2)
|
||||
@ -389,10 +388,10 @@ GEM
|
||||
multi_json (~> 1.15)
|
||||
rack (>= 2.2, < 4)
|
||||
rdf (~> 3.3)
|
||||
json-ld-preloaded (3.2.2)
|
||||
json-ld (~> 3.2)
|
||||
rdf (~> 3.2)
|
||||
json-schema (4.0.0)
|
||||
json-ld-preloaded (3.3.0)
|
||||
json-ld (~> 3.3)
|
||||
rdf (~> 3.3)
|
||||
json-schema (4.1.1)
|
||||
addressable (>= 2.8)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.7.1)
|
||||
@ -452,25 +451,25 @@ GEM
|
||||
memory_profiler (1.0.1)
|
||||
mime-types (3.5.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0808)
|
||||
mime-types-data (3.2023.1003)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.4)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.20.0)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.3.0)
|
||||
mutex_m (0.1.2)
|
||||
net-http (0.3.2)
|
||||
net-http (0.4.0)
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.1)
|
||||
net-imap (0.4.4)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.18.0)
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.1)
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.4.0)
|
||||
net-protocol
|
||||
@ -528,7 +527,7 @@ GEM
|
||||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
private_address_check (0.5.0)
|
||||
psych (5.1.1)
|
||||
psych (5.1.1.1)
|
||||
stringio
|
||||
public_suffix (5.0.3)
|
||||
puma (6.4.0)
|
||||
@ -559,20 +558,20 @@ GEM
|
||||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.1)
|
||||
actioncable (= 7.1.1)
|
||||
actionmailbox (= 7.1.1)
|
||||
actionmailer (= 7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
actiontext (= 7.1.1)
|
||||
actionview (= 7.1.1)
|
||||
activejob (= 7.1.1)
|
||||
activemodel (= 7.1.1)
|
||||
activerecord (= 7.1.1)
|
||||
activestorage (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
rails (7.1.2)
|
||||
actioncable (= 7.1.2)
|
||||
actionmailbox (= 7.1.2)
|
||||
actionmailer (= 7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
actiontext (= 7.1.2)
|
||||
actionview (= 7.1.2)
|
||||
activejob (= 7.1.2)
|
||||
activemodel (= 7.1.2)
|
||||
activerecord (= 7.1.2)
|
||||
activestorage (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.1.1)
|
||||
railties (= 7.1.2)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
@ -587,9 +586,9 @@ GEM
|
||||
rails-i18n (7.0.8)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.1.1)
|
||||
actionpack (= 7.1.1)
|
||||
activesupport (= 7.1.1)
|
||||
railties (7.1.2)
|
||||
actionpack (= 7.1.2)
|
||||
activesupport (= 7.1.2)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
@ -689,13 +688,13 @@ GEM
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
safety_net_attestation (0.4.0)
|
||||
jwt (~> 2.0)
|
||||
sanitize (6.0.2)
|
||||
sanitize (6.1.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
scenic (1.7.0)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
selenium-webdriver (4.13.1)
|
||||
selenium-webdriver (4.15.0)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
@ -710,7 +709,7 @@ GEM
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 6, < 8)
|
||||
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)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
redis (< 5.0)
|
||||
@ -718,7 +717,7 @@ GEM
|
||||
thor (>= 0.20, < 3.0)
|
||||
simple-navigation (4.4.0)
|
||||
activesupport (>= 2.3.2)
|
||||
simple_form (5.2.0)
|
||||
simple_form (5.3.0)
|
||||
actionpack (>= 5.2)
|
||||
activemodel (>= 5.2)
|
||||
simplecov (0.22.0)
|
||||
@ -739,8 +738,8 @@ GEM
|
||||
statsd-ruby (1.5.0)
|
||||
stoplight (3.0.2)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.0.8)
|
||||
strong_migrations (1.3.0)
|
||||
stringio (3.0.9)
|
||||
strong_migrations (1.6.4)
|
||||
activerecord (>= 5.2)
|
||||
swd (1.3.0)
|
||||
activesupport (>= 3)
|
||||
@ -753,9 +752,9 @@ GEM
|
||||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
test-prof (1.2.3)
|
||||
thor (1.2.2)
|
||||
thor (1.3.0)
|
||||
tilt (2.3.0)
|
||||
timeout (0.4.0)
|
||||
timeout (0.4.1)
|
||||
tpm-key_attestation (0.12.0)
|
||||
bindata (~> 2.4)
|
||||
openssl (> 2.0)
|
||||
@ -858,7 +857,7 @@ DEPENDENCIES
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.4.0)
|
||||
fog-openstack (~> 0.3)
|
||||
fog-openstack (~> 1.0)
|
||||
fuubar (~> 2.5)
|
||||
haml-rails (~> 2.0)
|
||||
haml_lint
|
||||
@ -883,7 +882,7 @@ DEPENDENCIES
|
||||
md-paperclip-azure (~> 2.2)
|
||||
memory_profiler
|
||||
mime-types (~> 3.5.0)
|
||||
net-http (~> 0.3.2)
|
||||
net-http (~> 0.4.0)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.15)
|
||||
nsa!
|
||||
@ -941,7 +940,7 @@ DEPENDENCIES
|
||||
sprockets-rails (~> 3.4)
|
||||
stackprof
|
||||
stoplight (~> 3.0.1)
|
||||
strong_migrations (= 1.3.0)
|
||||
strong_migrations (= 1.6.4)
|
||||
test-prof
|
||||
thor (~> 1.2)
|
||||
tty-prompt (~> 0.23)
|
||||
|
4
Vagrantfile
vendored
4
Vagrantfile
vendored
@ -112,11 +112,11 @@ bundle install
|
||||
|
||||
# Install node modules
|
||||
sudo corepack enable
|
||||
yarn set version classic
|
||||
corepack prepare
|
||||
yarn install
|
||||
|
||||
# Build Mastodon
|
||||
export RAILS_ENV=development
|
||||
export RAILS_ENV=development
|
||||
export $(cat ".env.vagrant" | xargs)
|
||||
bundle exec rails db:setup
|
||||
|
||||
|
@ -53,7 +53,7 @@ class PublicStatusesIndex < Chewy::Index
|
||||
index_scope ::Status.unscoped
|
||||
.kept
|
||||
.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
|
||||
field(:id, type: 'long')
|
||||
|
@ -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
|
||||
field(:id, type: 'long')
|
||||
|
@ -31,6 +31,11 @@ module Admin
|
||||
|
||||
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
|
||||
params.require(:admin_status_batch_action).permit(status_ids: [])
|
||||
end
|
||||
|
@ -7,6 +7,7 @@ class Api::BaseController < ApplicationController
|
||||
include RateLimitHeaders
|
||||
include AccessTokenTrackingConcern
|
||||
include ApiCachingConcern
|
||||
include Api::ContentSecurityPolicy
|
||||
|
||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||
|
||||
@ -17,26 +18,6 @@ class Api::BaseController < ApplicationController
|
||||
|
||||
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|
|
||||
render json: { error: e.to_s }, status: 422
|
||||
end
|
||||
|
@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
|
||||
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
|
||||
# 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
|
||||
end
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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 -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
|
||||
before_action -> { doorkeeper_authorize! :follow, :write, :'write:mutes' }, only: [:mute, :unmute]
|
||||
@ -90,18 +92,14 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def check_enabled_registrations
|
||||
forbidden if single_user_mode? || omniauth_only? || !allowed_registrations?
|
||||
end
|
||||
|
||||
def allowed_registrations?
|
||||
Setting.registrations_mode != 'none'
|
||||
end
|
||||
|
||||
def omniauth_only?
|
||||
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||
forbidden unless allowed_registration?(request.remote_ip, invite)
|
||||
end
|
||||
end
|
||||
|
@ -41,10 +41,10 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||
account: :account_stat,
|
||||
last_status: [
|
||||
:media_attachments,
|
||||
:preview_cards,
|
||||
:status_stat,
|
||||
:tags,
|
||||
{
|
||||
preview_cards_status: :preview_card,
|
||||
active_mentions: [account: :account_stat],
|
||||
account: :account_stat,
|
||||
},
|
||||
|
@ -1,12 +1,8 @@
|
||||
# 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!
|
||||
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
|
||||
vary_by ''
|
||||
|
||||
def show
|
||||
cache_even_if_authenticated!
|
||||
render_with_cache json: :activity, expires_in: 1.day
|
||||
|
8
app/controllers/api/v1/instances/base_controller.rb
Normal file
8
app/controllers/api/v1/instances/base_controller.rb
Normal 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
|
@ -1,8 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::DomainBlocksController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
|
||||
class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseController
|
||||
before_action :require_enabled_api!
|
||||
before_action :set_domain_blocks
|
||||
|
||||
@ -15,7 +13,7 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
|
||||
cache_if_unauthenticated!
|
||||
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
|
||||
|
||||
private
|
||||
@ -27,4 +25,16 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
|
||||
def set_domain_blocks
|
||||
@domain_blocks = DomainBlock.with_user_facing_limitations.by_severity
|
||||
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
|
||||
|
@ -1,13 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
class Api::V1::Instances::ExtendedDescriptionsController < Api::V1::Instances::BaseController
|
||||
skip_around_action :set_locale
|
||||
|
||||
before_action :set_extended_description
|
||||
|
||||
vary_by ''
|
||||
|
||||
# Override `current_user` to avoid reading session cookies unless in whitelist mode
|
||||
def current_user
|
||||
super if limited_federation_mode?
|
||||
|
@ -1,13 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::LanguagesController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
class Api::V1::Instances::LanguagesController < Api::V1::Instances::BaseController
|
||||
skip_around_action :set_locale
|
||||
|
||||
before_action :set_languages
|
||||
|
||||
vary_by ''
|
||||
|
||||
def show
|
||||
cache_even_if_authenticated!
|
||||
render json: @languages, each_serializer: REST::LanguageSerializer
|
||||
|
@ -1,13 +1,10 @@
|
||||
# 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!
|
||||
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
skip_around_action :set_locale
|
||||
|
||||
vary_by ''
|
||||
|
||||
# Override `current_user` to avoid reading session cookies unless in whitelist mode
|
||||
def current_user
|
||||
super if limited_federation_mode?
|
||||
|
@ -1,12 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
|
||||
class Api::V1::Instances::PrivacyPoliciesController < Api::V1::Instances::BaseController
|
||||
before_action :set_privacy_policy
|
||||
|
||||
vary_by ''
|
||||
|
||||
def show
|
||||
cache_even_if_authenticated!
|
||||
render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer
|
||||
|
@ -1,13 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::RulesController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
class Api::V1::Instances::RulesController < Api::V1::Instances::BaseController
|
||||
skip_around_action :set_locale
|
||||
|
||||
before_action :set_rules
|
||||
|
||||
vary_by ''
|
||||
|
||||
# Override `current_user` to avoid reading session cookies unless in whitelist mode
|
||||
def current_user
|
||||
super if limited_federation_mode?
|
||||
|
@ -1,12 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Instances::TranslationLanguagesController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
|
||||
class Api::V1::Instances::TranslationLanguagesController < Api::V1::Instances::BaseController
|
||||
before_action :set_languages
|
||||
|
||||
vary_by ''
|
||||
|
||||
def show
|
||||
cache_even_if_authenticated!
|
||||
render json: @languages
|
||||
|
30
app/controllers/api/v1/invites_controller.rb
Normal file
30
app/controllers/api/v1/invites_controller.rb
Normal 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
|
16
app/controllers/api/v1/statuses/base_controller.rb
Normal file
16
app/controllers/api/v1/statuses/base_controller.rb
Normal 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
|
@ -1,11 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::BookmarksController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:bookmarks' }
|
||||
before_action :require_user!
|
||||
before_action :set_status, only: [:create]
|
||||
skip_before_action :set_status, only: [:destroy]
|
||||
|
||||
def create
|
||||
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
|
||||
not_found
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::FavouritedByAccountsController < Api::V1::Statuses::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
|
||||
before_action :set_status
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
def index
|
||||
@ -61,13 +58,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
||||
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
||||
end
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
|
@ -1,11 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::FavouritesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:favourites' }
|
||||
before_action :require_user!
|
||||
before_action :set_status, only: [:create]
|
||||
skip_before_action :set_status, only: [:destroy]
|
||||
|
||||
def create
|
||||
FavouriteService.new.call(current_account, @status)
|
||||
@ -30,13 +28,4 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::HistoriesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::HistoriesController < Api::V1::Statuses::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||
before_action :set_status
|
||||
|
||||
def show
|
||||
cache_if_unauthenticated!
|
||||
@ -16,11 +13,4 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
|
||||
def status_edits
|
||||
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
|
||||
end
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
@ -1,11 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::MutesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::MutesController < Api::V1::Statuses::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:mutes' }
|
||||
before_action :require_user!
|
||||
before_action :set_status
|
||||
before_action :set_conversation
|
||||
|
||||
def create
|
||||
@ -24,13 +21,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation = @status.conversation
|
||||
raise Mastodon::ValidationError if @conversation.nil?
|
||||
|
@ -1,11 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::PinsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::PinsController < Api::V1::Statuses::BaseController
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
|
||||
before_action :require_user!
|
||||
before_action :set_status
|
||||
|
||||
def create
|
||||
StatusPin.create!(account: current_account, status: @status)
|
||||
@ -26,10 +23,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
end
|
||||
|
||||
def distribute_add_activity!
|
||||
json = ActiveModelSerializers::SerializableResource.new(
|
||||
@status,
|
||||
|
@ -1,10 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
|
||||
before_action :set_status
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
def index
|
||||
@ -57,13 +54,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
||||
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
||||
end
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
|
@ -1,13 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::ReblogsController < Api::BaseController
|
||||
include Authorization
|
||||
class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
||||
include Redisable
|
||||
include Lockable
|
||||
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
|
||||
before_action :require_user!
|
||||
before_action :set_reblog, only: [:create]
|
||||
skip_before_action :set_status
|
||||
|
||||
override_rate_limit_headers :create, family: :statuses
|
||||
|
||||
|
@ -1,21 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::SourcesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::SourcesController < Api::V1::Statuses::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||
before_action :set_status
|
||||
|
||||
def show
|
||||
render json: @status, serializer: REST::StatusSourceSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Statuses::TranslationsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||
before_action :set_status
|
||||
before_action :set_translation
|
||||
|
||||
rescue_from TranslationService::NotConfiguredError, with: :not_found
|
||||
@ -24,13 +21,6 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_status
|
||||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_translation
|
||||
@translation = TranslateStatusService.new.call(@status, content_locale)
|
||||
end
|
||||
|
@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
include RegistrationHelper
|
||||
include RegistrationSpamConcern
|
||||
|
||||
layout :determine_layout
|
||||
@ -83,19 +84,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
end
|
||||
|
||||
def check_enabled_registrations
|
||||
redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations? || ip_blocked?
|
||||
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?
|
||||
redirect_to root_path unless allowed_registration?(request.remote_ip, @invite)
|
||||
end
|
||||
|
||||
def invite_code
|
||||
|
27
app/controllers/concerns/api/content_security_policy.rb
Normal file
27
app/controllers/concerns/api/content_security_policy.rb
Normal 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
|
@ -6,7 +6,7 @@ module Admin::AccountModerationNotesHelper
|
||||
|
||||
link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
|
||||
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'),
|
||||
], ' ')
|
||||
end
|
||||
|
@ -91,6 +91,14 @@ module ApplicationHelper
|
||||
end
|
||||
end
|
||||
|
||||
def html_title
|
||||
safe_join(
|
||||
[content_for(:page_title).to_s.chomp, title]
|
||||
.select(&:present?),
|
||||
' - '
|
||||
)
|
||||
end
|
||||
|
||||
def title
|
||||
Rails.env.production? ? site_title : "#{site_title} (Dev)"
|
||||
end
|
||||
|
@ -298,5 +298,3 @@ module LanguagesHelper
|
||||
locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:enable Metrics/ModuleLength
|
||||
|
21
app/helpers/registration_helper.rb
Normal file
21
app/helpers/registration_helper.rb
Normal 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
|
@ -25,7 +25,7 @@ module SettingsHelper
|
||||
return if account.nil?
|
||||
|
||||
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
|
||||
|
@ -567,7 +567,7 @@ export function fetchRelationships(accountIds) {
|
||||
|
||||
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));
|
||||
}).catch(error => {
|
||||
dispatch(fetchRelationshipsFail(error));
|
||||
|
@ -119,7 +119,7 @@ class Account extends ImmutablePureComponent {
|
||||
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
|
||||
} else if (defaultAction === 'block') {
|
||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
|
||||
} else if (!account.get('moved') || following) {
|
||||
} else if (!account.get('suspended') && !account.get('moved') || following) {
|
||||
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ class Header extends ImmutablePureComponent {
|
||||
actionBtn = '';
|
||||
}
|
||||
|
||||
if (suspended && !account.getIn(['relationship', 'following'])) {
|
||||
if (account.get('suspended') && !account.getIn(['relationship', 'following'])) {
|
||||
actionBtn = '';
|
||||
}
|
||||
|
||||
@ -217,7 +217,7 @@ class Header extends ImmutablePureComponent {
|
||||
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.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||
menu.push(null);
|
||||
@ -228,7 +228,7 @@ class Header extends ImmutablePureComponent {
|
||||
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(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.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) {
|
||||
@ -340,18 +342,16 @@ class Header extends ImmutablePureComponent {
|
||||
{role}
|
||||
</a>
|
||||
|
||||
{!suspended && (
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{!hidden && (
|
||||
<>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
</>
|
||||
)}
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{!hidden && (
|
||||
<>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account__header__tabs__name'>
|
||||
|
@ -200,7 +200,7 @@ class ListTimeline extends PureComponent {
|
||||
</div>
|
||||
|
||||
<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'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
|
@ -11,6 +11,7 @@ import { throttle } from 'lodash';
|
||||
|
||||
import { Blurhash } from 'flavours/glitch/components/blurhash';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { playerSettings } from 'flavours/glitch/settings';
|
||||
|
||||
import { displayMedia, useBlurhash } from '../../initial_state';
|
||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||
@ -221,8 +222,8 @@ class Video extends PureComponent {
|
||||
|
||||
if(!isNaN(x)) {
|
||||
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
|
||||
this.video.volume = x;
|
||||
this.video.muted = this.state.muted;
|
||||
this._syncVideoToVolumeState(x);
|
||||
this._saveVolumeState(x);
|
||||
});
|
||||
}
|
||||
}, 15);
|
||||
@ -360,6 +361,8 @@ class Video extends PureComponent {
|
||||
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
|
||||
this._syncVideoFromLocalStorage();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@ -432,8 +435,28 @@ class Video extends PureComponent {
|
||||
const muted = !(this.video.muted || this.state.volume === 0);
|
||||
|
||||
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
|
||||
this.video.volume = this.state.volume;
|
||||
this.video.muted = this.state.muted;
|
||||
this._syncVideoToVolumeState();
|
||||
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 = () => {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
this._saveVolumeState(this.video.volume, this.video.muted);
|
||||
};
|
||||
|
||||
handleOpenVideo = () => {
|
||||
|
@ -46,4 +46,5 @@ export default class Settings {
|
||||
export const pushNotificationsSetting = new Settings('mastodon_push_notification_data');
|
||||
export const tagHistory = new Settings('mastodon_tag_history');
|
||||
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');
|
||||
|
@ -36,6 +36,7 @@
|
||||
|
||||
.modal-root__modal {
|
||||
pointer-events: auto;
|
||||
user-select: text;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
@ -460,7 +460,7 @@ export function fetchRelationships(accountIds) {
|
||||
|
||||
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 }));
|
||||
}).catch(error => {
|
||||
dispatch(fetchRelationshipsFail(error));
|
||||
|
@ -42,4 +42,5 @@ export interface ApiAccountJSON {
|
||||
suspended?: boolean;
|
||||
limited?: boolean;
|
||||
memorial?: boolean;
|
||||
hide_collections: boolean;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
||||
}
|
||||
>
|
||||
<img
|
||||
alt="alice"
|
||||
alt=""
|
||||
src="/animated/alice.gif"
|
||||
/>
|
||||
</div>
|
||||
@ -32,7 +32,7 @@ exports[`<Avatar /> Still renders a still avatar 1`] = `
|
||||
}
|
||||
>
|
||||
<img
|
||||
alt="alice"
|
||||
alt=""
|
||||
src="/static/alice.jpg"
|
||||
/>
|
||||
</div>
|
||||
|
@ -119,7 +119,7 @@ class Account extends ImmutablePureComponent {
|
||||
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
|
||||
} else if (defaultAction === 'block') {
|
||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
|
||||
} else if (!account.get('moved') || following) {
|
||||
} else if (!account.get('suspended') && !account.get('moved') || following) {
|
||||
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ export const Avatar: React.FC<Props> = ({
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={style}
|
||||
>
|
||||
{src && <img src={src} alt={account?.get('acct')} />}
|
||||
{src && <img src={src} alt='' />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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 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 { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
@ -348,6 +350,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
let replyIcon;
|
||||
let replyIconComponent;
|
||||
let replyTitle;
|
||||
|
||||
if (status.get('in_reply_to_id', null) === null) {
|
||||
replyIcon = 'reply';
|
||||
replyIconComponent = ReplyIcon;
|
||||
@ -360,15 +363,20 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
|
||||
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
|
||||
|
||||
let reblogTitle = '';
|
||||
let reblogTitle, reblogIconComponent;
|
||||
|
||||
if (status.get('reblogged')) {
|
||||
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
||||
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
|
||||
} else if (publicStatus) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog);
|
||||
reblogIconComponent = RepeatIcon;
|
||||
} else if (reblogPrivate) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog_private);
|
||||
reblogIconComponent = RepeatPrivateIcon;
|
||||
} else {
|
||||
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
||||
reblogIconComponent = RepeatDisabledIcon;
|
||||
}
|
||||
|
||||
const filterButton = this.props.onFilter && (
|
||||
@ -380,7 +388,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||
return (
|
||||
<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={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 bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' iconComponent={status.get('bookmarked') ? BookmarkIcon : BookmarkBorderIcon} onClick={this.handleBookmarkClick} />
|
||||
|
||||
|
80
app/javascript/mastodon/features/__tests__/toggle-play.jsx
Normal file
80
app/javascript/mastodon/features/__tests__/toggle-play.jsx
Normal 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/);
|
||||
});
|
||||
});
|
@ -289,7 +289,7 @@ class Header extends ImmutablePureComponent {
|
||||
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.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||
menu.push(null);
|
||||
@ -299,7 +299,7 @@ class Header extends ImmutablePureComponent {
|
||||
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(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.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) {
|
||||
@ -395,7 +397,7 @@ class Header extends ImmutablePureComponent {
|
||||
|
||||
<div className='account__header__image'>
|
||||
<div className='account__header__info'>
|
||||
{!suspended && info}
|
||||
{info}
|
||||
</div>
|
||||
|
||||
{!(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} />
|
||||
</a>
|
||||
|
||||
{!suspended && (
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{!hidden && (
|
||||
<>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
</>
|
||||
)}
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{!hidden && (
|
||||
<>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account__header__tabs__name'>
|
||||
|
@ -178,7 +178,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
modalType: 'IMAGE',
|
||||
modalProps: {
|
||||
src: account.get('avatar'),
|
||||
alt: account.get('acct'),
|
||||
alt: '',
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
@ -20,6 +20,7 @@ import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/featur
|
||||
|
||||
import { Blurhash } from '../../components/blurhash';
|
||||
import { displayMedia, useBlurhash } from '../../initial_state';
|
||||
import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
|
||||
|
||||
import Visualizer from './visualizer';
|
||||
|
||||
@ -165,15 +166,32 @@ class Audio extends PureComponent {
|
||||
}
|
||||
|
||||
togglePlay = () => {
|
||||
if (!this.audioContext) {
|
||||
this._initAudioContext();
|
||||
const audios = document.querySelectorAll('audio');
|
||||
|
||||
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.setState({ paused: false }, () => this.audio.play());
|
||||
} else {
|
||||
this.setState({ paused: true }, () => this.audio.pause());
|
||||
}
|
||||
this.audio.play();
|
||||
setCurrentMedia(this.audio);
|
||||
};
|
||||
|
||||
handleResize = debounce(() => {
|
||||
@ -195,6 +213,7 @@ class Audio extends PureComponent {
|
||||
};
|
||||
|
||||
handlePause = () => {
|
||||
this.audio.pause();
|
||||
this.setState({ paused: true });
|
||||
|
||||
if (this.audioContext) {
|
||||
|
@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
hasMore: !!state.getIn(['user_lists', 'followers', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'followers', accountId, 'isLoading'], true),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
@ -111,7 +112,7 @@ class Followers extends ImmutablePureComponent {
|
||||
}, 300, { leading: true });
|
||||
|
||||
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) {
|
||||
return (
|
||||
@ -137,6 +138,8 @@ class Followers extends ImmutablePureComponent {
|
||||
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||
} else if (blockedBy) {
|
||||
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()) {
|
||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||
} else {
|
||||
|
@ -45,6 +45,7 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
|
||||
hasMore: !!state.getIn(['user_lists', 'following', accountId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'following', accountId, 'isLoading'], true),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
hideCollections: state.getIn(['accounts', accountId, 'hide_collections'], false),
|
||||
hidden: getAccountHidden(state, accountId),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
@ -111,7 +112,7 @@ class Following extends ImmutablePureComponent {
|
||||
}, 300, { leading: true });
|
||||
|
||||
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) {
|
||||
return (
|
||||
@ -137,6 +138,8 @@ class Following extends ImmutablePureComponent {
|
||||
emptyMessage = <LimitedAccountHint accountId={accountId} />;
|
||||
} else if (blockedBy) {
|
||||
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()) {
|
||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||
} else {
|
||||
|
@ -204,7 +204,7 @@ class ListTimeline extends PureComponent {
|
||||
</div>
|
||||
|
||||
<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'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
|
@ -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 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 { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
@ -280,6 +282,7 @@ class ActionBar extends PureComponent {
|
||||
|
||||
let replyIcon;
|
||||
let replyIconComponent;
|
||||
|
||||
if (status.get('in_reply_to_id', null) === null) {
|
||||
replyIcon = 'reply';
|
||||
replyIconComponent = ReplyIcon;
|
||||
@ -290,21 +293,26 @@ class ActionBar extends PureComponent {
|
||||
|
||||
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
|
||||
|
||||
let reblogTitle;
|
||||
let reblogTitle, reblogIconComponent;
|
||||
|
||||
if (status.get('reblogged')) {
|
||||
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
||||
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
|
||||
} else if (publicStatus) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog);
|
||||
reblogIconComponent = RepeatIcon;
|
||||
} else if (reblogPrivate) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog_private);
|
||||
reblogIconComponent = RepeatPrivateIcon;
|
||||
} else {
|
||||
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
||||
reblogIconComponent = RepeatDisabledIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<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 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='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>
|
||||
|
||||
|
@ -19,8 +19,10 @@ import { throttle } from 'lodash';
|
||||
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { playerSettings } from 'mastodon/settings';
|
||||
|
||||
import { displayMedia, useBlurhash } from '../../initial_state';
|
||||
import { currentMedia, setCurrentMedia } from '../../reducers/media_attachments';
|
||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
|
||||
|
||||
const messages = defineMessages({
|
||||
@ -180,6 +182,7 @@ class Video extends PureComponent {
|
||||
};
|
||||
|
||||
handlePause = () => {
|
||||
this.video.pause();
|
||||
this.setState({ paused: true });
|
||||
};
|
||||
|
||||
@ -226,8 +229,8 @@ class Video extends PureComponent {
|
||||
|
||||
if(!isNaN(x)) {
|
||||
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
|
||||
this.video.volume = x;
|
||||
this.video.muted = this.state.muted;
|
||||
this._syncVideoToVolumeState(x);
|
||||
this._saveVolumeState(x);
|
||||
});
|
||||
}
|
||||
}, 15);
|
||||
@ -343,11 +346,32 @@ class Video extends PureComponent {
|
||||
};
|
||||
|
||||
togglePlay = () => {
|
||||
if (this.state.paused) {
|
||||
this.setState({ paused: false }, () => this.video.play());
|
||||
} else {
|
||||
this.setState({ paused: true }, () => this.video.pause());
|
||||
const videos = document.querySelectorAll('video');
|
||||
|
||||
videos.forEach((video) => {
|
||||
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 = () => {
|
||||
@ -365,6 +389,8 @@ class Video extends PureComponent {
|
||||
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
|
||||
this._syncVideoFromLocalStorage();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
@ -437,8 +463,28 @@ class Video extends PureComponent {
|
||||
const muted = !(this.video.muted || this.state.volume === 0);
|
||||
|
||||
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
|
||||
this.video.volume = this.state.volume;
|
||||
this.video.muted = this.state.muted;
|
||||
this._syncVideoToVolumeState();
|
||||
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 = () => {
|
||||
this.setState({ volume: this.video.volume, muted: this.video.muted });
|
||||
this._saveVolumeState(this.video.volume, this.video.muted);
|
||||
};
|
||||
|
||||
handleOpenVideo = () => {
|
||||
|
@ -14,6 +14,7 @@
|
||||
"account.badges.group": "Groep",
|
||||
"account.block": "Blokkeer @{name}",
|
||||
"account.block_domain": "Blokkeer domein {domain}",
|
||||
"account.block_short": "Blokkeer",
|
||||
"account.blocked": "Geblokkeer",
|
||||
"account.browse_more_on_origin_server": "Verken die oorspronklike profiel",
|
||||
"account.cancel_follow_request": "Herroep volgversoek",
|
||||
@ -45,6 +46,7 @@
|
||||
"account.posts_with_replies": "Plasings en antwoorde",
|
||||
"account.report": "Rapporteer @{name}",
|
||||
"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.show_reblogs": "Wys aangestuurde plasings van @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} Plaas} other {{counter} Plasings}}",
|
||||
@ -82,6 +84,7 @@
|
||||
"column.community": "Plaaslike tydlyn",
|
||||
"column.directory": "Blaai deur profiele",
|
||||
"column.domain_blocks": "Geblokkeerde domeine",
|
||||
"column.favourites": "Gunstelinge",
|
||||
"column.follow_requests": "Volgversoeke",
|
||||
"column.home": "Tuis",
|
||||
"column.lists": "Lyste",
|
||||
@ -271,6 +274,7 @@
|
||||
"privacy.unlisted.short": "Ongelys",
|
||||
"privacy_policy.last_updated": "Laaste bywerking op {date}",
|
||||
"privacy_policy.title": "Privaatheidsbeleid",
|
||||
"regeneration_indicator.sublabel": "Jou tuis-voer word voorberei!",
|
||||
"reply_indicator.cancel": "Kanselleer",
|
||||
"report.placeholder": "Type or paste additional comments",
|
||||
"report.submit": "Submit report",
|
||||
|
@ -201,7 +201,7 @@
|
||||
"disabled_account_banner.text": "Ваш уліковы запіс {disabledAccount} часова адключаны.",
|
||||
"dismissable_banner.community_timeline": "Гэта самыя апошнія допісы ад людзей, уліковыя запісы якіх размяшчаюцца на {domain}.",
|
||||
"dismissable_banner.dismiss": "Адхіліць",
|
||||
"dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца прама зараз на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
|
||||
"dismissable_banner.explore_links": "Гэтыя навіны абмяркоўваюцца цяпер на гэтым і іншых серверах дэцэнтралізаванай сеткі.",
|
||||
"dismissable_banner.explore_statuses": "Допісы з гэтага і іншых сервераў дэцэнтралізаванай сеткі, якія набіраюць папулярнасць прама зараз.",
|
||||
"dismissable_banner.explore_tags": "Гэтыя хэштэгі зараз набіраюць папулярнасць сярод людзей на гэтым і іншых серверах дэцэнтралізаванай сеткі",
|
||||
"dismissable_banner.public_timeline": "Гэта апошнія публічныя допісы людзей з усей сеткі, за якімі сочаць карыстальнікі {domain}.",
|
||||
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Вынікі пошуку",
|
||||
"emoji_button.symbols": "Сімвалы",
|
||||
"emoji_button.travel": "Падарожжы і месцы",
|
||||
"empty_column.account_hides_collections": "Гэты карыстальнік вырашыў схаваць гэтую інфармацыю",
|
||||
"empty_column.account_suspended": "Уліковы запіс прыпынены",
|
||||
"empty_column.account_timeline": "Тут няма допісаў!",
|
||||
"empty_column.account_unavailable": "Профіль недаступны",
|
||||
@ -481,7 +482,7 @@
|
||||
"onboarding.share.lead": "Дайце людзям ведаць, як яны могуць знайсці вас на Mastodon!",
|
||||
"onboarding.share.message": "Я {username} на #Mastodon! Сачыце за мной на {url}",
|
||||
"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.skip": "Want to skip right ahead?",
|
||||
"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.title": "Customize your profile",
|
||||
"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.accounts_from_other_servers": "<strong>Ці вы ведаеце?</strong> Паколькі Mastodon дэцэнтралізаваны, некаторыя профілі, якія вам трапляюцца, будуць размяшчацца на іншых серверах, адрозных ад вашага. І ўсё ж вы можаце бесперашкодна ўзаемадзейнічаць з імі! Іх сервер пазначаны ў другой палове імя карыстальніка!",
|
||||
"onboarding.tips.migration": "<strong>Ці вы ведаеце?</strong> Калі вы адчуваеце, што {domain} не з'яўляецца для вас лепшым выбарам у будучыні, вы можаце перайсці на іншы сервер Mastodon, не губляючы сваіх падпісчыкаў. Вы нават можаце стварыць свой уласны сервер!",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Resultats de la cerca",
|
||||
"emoji_button.symbols": "Símbols",
|
||||
"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_timeline": "No hi ha tuts aquí!",
|
||||
"empty_column.account_unavailable": "Perfil no disponible",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Canlyniadau chwilio",
|
||||
"emoji_button.symbols": "Symbolau",
|
||||
"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_timeline": "Dim postiadau yma!",
|
||||
"empty_column.account_unavailable": "Nid yw'r proffil ar gael",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Søgeresultater",
|
||||
"emoji_button.symbols": "Symboler",
|
||||
"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_timeline": "Ingen indlæg hér!",
|
||||
"empty_column.account_unavailable": "Profil utilgængelig",
|
||||
|
@ -88,7 +88,7 @@
|
||||
"attachments_list.unprocessed": "(ausstehend)",
|
||||
"audio.hide": "Audio ausblenden",
|
||||
"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.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!",
|
||||
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Suchergebnisse",
|
||||
"emoji_button.symbols": "Symbole",
|
||||
"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_timeline": "Keine Beiträge vorhanden!",
|
||||
"empty_column.account_unavailable": "Profil nicht verfügbar",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Search results",
|
||||
"emoji_button.symbols": "Symbols",
|
||||
"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_timeline": "No posts here!",
|
||||
"empty_column.account_unavailable": "Profile unavailable",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Resultados de búsqueda",
|
||||
"emoji_button.symbols": "Símbolos",
|
||||
"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_timeline": "¡No hay mensajes acá!",
|
||||
"empty_column.account_unavailable": "Perfil no disponible",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Resultados de búsqueda",
|
||||
"emoji_button.symbols": "Símbolos",
|
||||
"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_timeline": "¡No hay toots aquí!",
|
||||
"empty_column.account_unavailable": "Perfil no disponible",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Resultados de búsqueda",
|
||||
"emoji_button.symbols": "Símbolos",
|
||||
"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_timeline": "¡No hay publicaciones aquí!",
|
||||
"empty_column.account_unavailable": "Perfil no disponible",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Otsitulemused",
|
||||
"emoji_button.symbols": "Sümbolid",
|
||||
"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_timeline": "Siin postitusi ei ole!",
|
||||
"empty_column.account_unavailable": "Profiil pole saadaval",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Bilaketaren emaitzak",
|
||||
"emoji_button.symbols": "Sinboloak",
|
||||
"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_timeline": "Ez dago bidalketarik hemen!",
|
||||
"empty_column.account_unavailable": "Profila ez dago eskuragarri",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "نتایج جستوجو",
|
||||
"emoji_button.symbols": "نمادها",
|
||||
"emoji_button.travel": "سفر و مکان",
|
||||
"empty_column.account_hides_collections": "کاربر خواسته که این اطّلاعات در دسترس نباشند",
|
||||
"empty_column.account_suspended": "حساب معلق شد",
|
||||
"empty_column.account_timeline": "هیچ فرستهای اینجا نیست!",
|
||||
"empty_column.account_unavailable": "نمایهٔ موجود نیست",
|
||||
@ -358,13 +359,13 @@
|
||||
"keyboard_shortcuts.profile": "گشودن نمایهٔ نویسنده",
|
||||
"keyboard_shortcuts.reply": "پاسخ به فرسته",
|
||||
"keyboard_shortcuts.requests": "گشودن سیاههٔ درخواستهای پیگیری",
|
||||
"keyboard_shortcuts.search": "تمرکز روی جستوجو",
|
||||
"keyboard_shortcuts.search": "تمرکز روی نوار جستوجو",
|
||||
"keyboard_shortcuts.spoilers": "نمایش/نهفتن زمینهٔ هشدار محتوا",
|
||||
"keyboard_shortcuts.start": "گشودن ستون «آغاز کنید»",
|
||||
"keyboard_shortcuts.toggle_hidden": "نمایش/نهفتن نوشتهٔ پشت هشدار محتوا",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "نمایش/نهفتن رسانه",
|
||||
"keyboard_shortcuts.toot": "شروع یک فرستهٔ جدید",
|
||||
"keyboard_shortcuts.unfocus": "برداشتن تمرکز از نوشتن/جستوجو",
|
||||
"keyboard_shortcuts.unfocus": "برداشتن تمرکز از ناحیهٔ نوشتن یا جستوجو",
|
||||
"keyboard_shortcuts.up": "بالا بردن در سیاهه",
|
||||
"lightbox.close": "بستن",
|
||||
"lightbox.compress": "فشردهسازی جعبهٔ نمایش تصویر",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Hakutulokset",
|
||||
"emoji_button.symbols": "Symbolit",
|
||||
"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_timeline": "Ei viestejä täällä.",
|
||||
"empty_column.account_unavailable": "Profiilia ei löydy",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Leitiúrslit",
|
||||
"emoji_button.symbols": "Ímyndir",
|
||||
"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_timeline": "Einki uppslag her!",
|
||||
"empty_column.account_unavailable": "Vangin er ikki tøkur",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Résultats",
|
||||
"emoji_button.symbols": "Symboles",
|
||||
"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_timeline": "Aucune publication ici!",
|
||||
"empty_column.account_unavailable": "Profil non disponible",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Résultats de la recherche",
|
||||
"emoji_button.symbols": "Symboles",
|
||||
"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_timeline": "Aucun message ici !",
|
||||
"empty_column.account_unavailable": "Profil non disponible",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Sykresultaten",
|
||||
"emoji_button.symbols": "Symboalen",
|
||||
"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_timeline": "Hjir binne gjin berjochten!",
|
||||
"empty_column.account_unavailable": "Profyl net beskikber",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Resultados da procura",
|
||||
"emoji_button.symbols": "Símbolos",
|
||||
"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_timeline": "Non hai publicacións aquí!",
|
||||
"empty_column.account_unavailable": "Perfil non dispoñible",
|
||||
|
@ -62,7 +62,7 @@
|
||||
"account.share": "שתף את הפרופיל של @{name}",
|
||||
"account.show_reblogs": "הצג הדהודים מאת @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {הודעה} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}",
|
||||
"account.unblock": "הסר את החסימה של @{name}",
|
||||
"account.unblock": "להסיר חסימה ל- @{name}",
|
||||
"account.unblock_domain": "הסירי את החסימה של קהילת {domain}",
|
||||
"account.unblock_short": "הסר חסימה",
|
||||
"account.unendorse": "אל תקדם בפרופיל",
|
||||
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "תוצאות חיפוש",
|
||||
"emoji_button.symbols": "סמלים",
|
||||
"emoji_button.travel": "טיולים ואתרים",
|
||||
"empty_column.account_hides_collections": "המשתמש.ת בחר.ה להסתיר מידע זה",
|
||||
"empty_column.account_suspended": "חשבון מושהה",
|
||||
"empty_column.account_timeline": "אין עדיין אף הודעה!",
|
||||
"empty_column.account_unavailable": "פרופיל לא זמין",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Keresési találatok",
|
||||
"emoji_button.symbols": "Szimbólumok",
|
||||
"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_timeline": "Itt nincs bejegyzés!",
|
||||
"empty_column.account_unavailable": "A profil nem érhető el",
|
||||
|
@ -1,8 +1,11 @@
|
||||
{
|
||||
"account.add_or_remove_from_list": "Tinye ma ọ bụ Wepu na ndepụta",
|
||||
"account.badges.bot": "Bot",
|
||||
"account.badges.group": "Otù",
|
||||
"account.cancel_follow_request": "Withdraw follow request",
|
||||
"account.follow": "Soro",
|
||||
"account.followers": "Ndị na-eso",
|
||||
"account.following": "Na-eso",
|
||||
"account.follows_you": "Na-eso gị",
|
||||
"account.mute": "Mee ogbi @{name}",
|
||||
"account.unfollow": "Kwụsị iso",
|
||||
@ -11,16 +14,20 @@
|
||||
"audio.hide": "Zoo ụda",
|
||||
"bundle_column_error.retry": "Nwaa ọzọ",
|
||||
"bundle_column_error.routing.title": "404",
|
||||
"bundle_modal_error.close": "Mechie",
|
||||
"bundle_modal_error.retry": "Nwaa ọzọ",
|
||||
"column.about": "Maka",
|
||||
"column.blocks": "Ojiarụ egbochiri",
|
||||
"column.bookmarks": "Ebenrụtụakā",
|
||||
"column.home": "Be",
|
||||
"column.lists": "Ndepụta",
|
||||
"column.pins": "Pinned post",
|
||||
"column_header.pin": "Gbado na profaịlụ gị",
|
||||
"column_subheading.settings": "Mwube",
|
||||
"community.column_settings.media_only": "Media only",
|
||||
"compose.language.change": "Gbanwee 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.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?",
|
||||
@ -32,7 +39,10 @@
|
||||
"confirmations.delete.message": "Are you sure you want to delete this status?",
|
||||
"confirmations.delete_list.confirm": "Hichapụ",
|
||||
"confirmations.domain_block.confirm": "Hide entire domain",
|
||||
"confirmations.edit.confirm": "Dezie",
|
||||
"confirmations.mute.confirm": "Mee ogbi",
|
||||
"confirmations.reply.confirm": "Zaa",
|
||||
"confirmations.unfollow.confirm": "Kwụsị iso",
|
||||
"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_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.up": "to move up in the list",
|
||||
"lists.delete": "Hichapụ ndepụta",
|
||||
"lists.edit": "Dezie ndepụta",
|
||||
"lists.subheading": "Ndepụta gị",
|
||||
"loading_indicator.label": "Na-adọnye...",
|
||||
"navigation_bar.about": "Maka",
|
||||
@ -100,20 +111,27 @@
|
||||
"privacy.change": "Adjust status privacy",
|
||||
"privacy.direct.short": "Direct",
|
||||
"privacy.private.short": "Followers-only",
|
||||
"relative_time.full.just_now": "kịta",
|
||||
"relative_time.just_now": "kịta",
|
||||
"relative_time.today": "taa",
|
||||
"reply_indicator.cancel": "Kagbuo",
|
||||
"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.submit": "Submit report",
|
||||
"report.target": "Report {target}",
|
||||
"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.learn_more": "Mụtakwuo",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"status.admin_status": "Open this status in the moderation interface",
|
||||
"status.bookmark": "Kee ebenrụtụakā",
|
||||
"status.copy": "Copy link to status",
|
||||
"status.delete": "Hichapụ",
|
||||
"status.edit": "Dezie",
|
||||
"status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}",
|
||||
"status.open": "Expand this status",
|
||||
"status.remove_bookmark": "Wepu ebenrụtụakā",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Leitarniðurstöður",
|
||||
"emoji_button.symbols": "Tákn",
|
||||
"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_timeline": "Engar færslur hér!",
|
||||
"empty_column.account_unavailable": "Notandasnið ekki tiltækt",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "Risultati della ricerca",
|
||||
"emoji_button.symbols": "Simboli",
|
||||
"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_timeline": "Nessun post qui!",
|
||||
"empty_column.account_unavailable": "Profilo non disponibile",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "検索結果",
|
||||
"emoji_button.symbols": "記号",
|
||||
"emoji_button.travel": "旅行と場所",
|
||||
"empty_column.account_hides_collections": "このユーザーはこの情報を開示しないことにしています。",
|
||||
"empty_column.account_suspended": "アカウントは停止されています",
|
||||
"empty_column.account_timeline": "投稿がありません!",
|
||||
"empty_column.account_unavailable": "プロフィールは利用できません",
|
||||
@ -585,8 +586,8 @@
|
||||
"search.no_recent_searches": "検索履歴はありません",
|
||||
"search.placeholder": "検索",
|
||||
"search.quick_action.account_search": "{x}に該当するプロフィール",
|
||||
"search.quick_action.go_to_account": "{x}のプロフィールを見る",
|
||||
"search.quick_action.go_to_hashtag": "{x}に該当するハッシュタグ",
|
||||
"search.quick_action.go_to_account": "プロフィール {x} を見る",
|
||||
"search.quick_action.go_to_hashtag": "ハッシュタグ {x} を見る",
|
||||
"search.quick_action.open_url": "MastodonでURLを開く",
|
||||
"search.quick_action.status_search": "{x}に該当する投稿",
|
||||
"search.search_or_paste": "検索またはURLを入力",
|
||||
|
@ -222,6 +222,7 @@
|
||||
"emoji_button.search_results": "검색 결과",
|
||||
"emoji_button.symbols": "기호",
|
||||
"emoji_button.travel": "여행과 장소",
|
||||
"empty_column.account_hides_collections": "이 사용자는 이 정보를 사용할 수 없도록 설정했습니다",
|
||||
"empty_column.account_suspended": "계정 정지됨",
|
||||
"empty_column.account_timeline": "이곳에는 게시물이 없습니다!",
|
||||
"empty_column.account_unavailable": "프로필 사용 불가",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user