Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
a7b905d573
498 changed files with 7515 additions and 4371 deletions
4
.env.development
Normal file
4
.env.development
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Required by ActiveRecord encryption feature
|
||||
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR
|
||||
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E
|
||||
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr
|
|
@ -3,3 +3,8 @@ NODE_ENV=production
|
|||
# Federation
|
||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||
LOCAL_HTTPS=true
|
||||
|
||||
# Required by ActiveRecord encryption feature
|
||||
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=fkSxKD2bF396kdQbrP1EJ7WbU7ZgNokR
|
||||
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=r0hvVmzBVsjxC7AMlwhOzmtc36ZCOS1E
|
||||
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=PhdFyyfy5xJ7WVd2lWBpcPScRQHzRTNr
|
||||
|
|
|
@ -380,6 +380,7 @@ module.exports = defineConfig({
|
|||
"message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/restrict-template-expressions": ['warn', { allowNumber: true }],
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
|
||||
// Those rules set stricter rules for TS files
|
||||
|
|
2
.github/workflows/crowdin-download.yml
vendored
2
.github/workflows/crowdin-download.yml
vendored
|
@ -53,7 +53,7 @@ jobs:
|
|||
|
||||
# Create or update the pull request
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6.0.2
|
||||
uses: peter-evans/create-pull-request@v6.0.4
|
||||
with:
|
||||
commit-message: 'New Crowdin translations'
|
||||
title: 'New Crowdin Translations (automated)'
|
||||
|
|
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
|
@ -38,5 +38,5 @@ jobs:
|
|||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
||||
- name: Jest testing
|
||||
- name: JavaScript testing
|
||||
run: yarn jest --reporters github-actions summary
|
||||
|
|
6
.github/workflows/test-ruby.yml
vendored
6
.github/workflows/test-ruby.yml
vendored
|
@ -28,6 +28,9 @@ jobs:
|
|||
env:
|
||||
RAILS_ENV: ${{ matrix.mode }}
|
||||
BUNDLE_WITH: ${{ matrix.mode }}
|
||||
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: precompile_placeholder
|
||||
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: precompile_placeholder
|
||||
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: precompile_placeholder
|
||||
OTP_SECRET: precompile_placeholder
|
||||
SECRET_KEY_BASE: precompile_placeholder
|
||||
|
||||
|
@ -111,7 +114,6 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
- '3.3'
|
||||
|
@ -187,7 +189,6 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
- '3.3'
|
||||
|
@ -287,7 +288,6 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
- '3.3'
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -24,7 +24,6 @@
|
|||
/public/packs-test
|
||||
.env
|
||||
.env.production
|
||||
.env.development
|
||||
/node_modules/
|
||||
/build/
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
exclude:
|
||||
- 'vendor/**/*'
|
||||
- lib/templates/haml/scaffold/_form.html.haml
|
||||
|
||||
require:
|
||||
- ./lib/linter/haml_middle_dot.rb
|
||||
|
@ -11,6 +10,6 @@ linters:
|
|||
MiddleDot:
|
||||
enabled: true
|
||||
LineLength:
|
||||
max: 320
|
||||
max: 300
|
||||
ViewLength:
|
||||
max: 200 # Override default value of 100 inherited from rubocop
|
||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
|||
20.11
|
||||
20.12
|
||||
|
|
27
.rubocop.yml
27
.rubocop.yml
|
@ -9,12 +9,13 @@ inherit_mode:
|
|||
require:
|
||||
- rubocop-rails
|
||||
- rubocop-rspec
|
||||
- rubocop-rspec_rails
|
||||
- rubocop-performance
|
||||
- rubocop-capybara
|
||||
- ./lib/linter/rubocop_middle_dot
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 3.0 # Set to minimum supported version of CI
|
||||
TargetRubyVersion: 3.1 # Set to minimum supported version of CI
|
||||
DisplayCopNames: true
|
||||
DisplayStyleGuide: true
|
||||
ExtraDetails: true
|
||||
|
@ -39,13 +40,7 @@ Layout/FirstHashElementIndentation:
|
|||
# Reason: Currently disabled in .rubocop_todo.yml
|
||||
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
|
||||
Layout/LineLength:
|
||||
Max: 320 # Default of 120 causes a duplicate entry in generated todo file
|
||||
|
||||
# Reason:
|
||||
# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier
|
||||
Lint/UselessAccessModifier:
|
||||
ContextCreatingMethods:
|
||||
- class_methods
|
||||
Max: 300 # Default of 120 causes a duplicate entry in generated todo file
|
||||
|
||||
## Disable most Metrics/*Length cops
|
||||
# Reason: those are often triggered and force significant refactors when this happend
|
||||
|
@ -86,6 +81,11 @@ Metrics/CyclomaticComplexity:
|
|||
Metrics/ParameterLists:
|
||||
CountKeywordArgs: false
|
||||
|
||||
# Reason: Prefer seeing a variable name
|
||||
# https://docs.rubocop.org/rubocop/cops_naming.html#namingblockforwarding
|
||||
Naming/BlockForwarding:
|
||||
EnforcedStyle: explicit
|
||||
|
||||
# Reason: Prevailing style is argument file paths
|
||||
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
|
||||
Rails/FilePath:
|
||||
|
@ -148,11 +148,6 @@ RSpec/NamedSubject:
|
|||
RSpec/NotToNot:
|
||||
EnforcedStyle: to_not
|
||||
|
||||
# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus
|
||||
# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus
|
||||
RSpec/Rails/HttpStatus:
|
||||
EnforcedStyle: numeric
|
||||
|
||||
# Reason: Match overrides from Rspec/FilePath rule above
|
||||
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat
|
||||
RSpec/SpecFilePathFormat:
|
||||
|
@ -163,6 +158,11 @@ RSpec/SpecFilePathFormat:
|
|||
OEmbedController: oembed_controller
|
||||
OStatus: ostatus
|
||||
|
||||
# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus
|
||||
# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus
|
||||
RSpecRails/HttpStatus:
|
||||
EnforcedStyle: numeric
|
||||
|
||||
# Reason:
|
||||
# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren
|
||||
Style/ClassAndModuleChildren:
|
||||
|
@ -186,6 +186,7 @@ Style/FormatStringToken:
|
|||
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax
|
||||
Style/HashSyntax:
|
||||
EnforcedStyle: ruby19_no_mixed_keys
|
||||
EnforcedShorthandSyntax: either
|
||||
|
||||
# Reason:
|
||||
# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.60.2.
|
||||
# using RuboCop version 1.62.1.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
|
||||
# Include: **/*.gemfile, **/Gemfile, **/gems.rb
|
||||
Bundler/OrderedGems:
|
||||
Exclude:
|
||||
- 'Gemfile'
|
||||
|
||||
Lint/NonLocalExitFromIterator:
|
||||
Exclude:
|
||||
- 'app/helpers/jsonld_helper.rb'
|
||||
|
@ -36,7 +29,7 @@ Metrics/PerceivedComplexity:
|
|||
|
||||
# Configuration parameters: CountAsOne.
|
||||
RSpec/ExampleLength:
|
||||
Max: 20 # Override default of 5
|
||||
Max: 18
|
||||
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 7
|
||||
|
@ -61,15 +54,6 @@ Rails/OutputSafety:
|
|||
Exclude:
|
||||
- 'config/initializers/simple_form.rb'
|
||||
|
||||
# Configuration parameters: Include.
|
||||
# Include: app/models/**/*.rb
|
||||
Rails/UniqueValidationWithoutIndex:
|
||||
Exclude:
|
||||
- 'app/models/account_alias.rb'
|
||||
- 'app/models/custom_filter_status.rb'
|
||||
- 'app/models/identity.rb'
|
||||
- 'app/models/webauthn_credential.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
# AllowedMethods: ==, equal?, eql?
|
||||
|
@ -88,7 +72,6 @@ Style/FetchEnvVar:
|
|||
Exclude:
|
||||
- 'app/lib/redis_configuration.rb'
|
||||
- 'app/lib/translation_service.rb'
|
||||
- 'config/environments/development.rb'
|
||||
- 'config/environments/production.rb'
|
||||
- 'config/initializers/2_limited_federation_mode.rb'
|
||||
- 'config/initializers/3_omniauth.rb'
|
||||
|
@ -98,7 +81,6 @@ Style/FetchEnvVar:
|
|||
- 'config/initializers/paperclip.rb'
|
||||
- 'config/initializers/vapid.rb'
|
||||
- 'lib/mastodon/redis_config.rb'
|
||||
- 'lib/premailer_webpack_strategy.rb'
|
||||
- 'lib/tasks/repo.rake'
|
||||
- 'spec/features/profile_spec.rb'
|
||||
|
||||
|
@ -144,7 +126,6 @@ Style/GuardClause:
|
|||
- 'lib/mastodon/cli/accounts.rb'
|
||||
- 'lib/mastodon/cli/maintenance.rb'
|
||||
- 'lib/mastodon/cli/media.rb'
|
||||
- 'lib/paperclip/attachment_extensions.rb'
|
||||
- 'lib/tasks/repo.rake'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
|
@ -207,13 +188,6 @@ Style/OptionalBooleanParameter:
|
|||
- 'app/workers/unfollow_follow_worker.rb'
|
||||
- 'lib/mastodon/redis_config.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: PreferredDelimiters.
|
||||
Style/PercentLiteralDelimiters:
|
||||
Exclude:
|
||||
- 'config/deploy.rb'
|
||||
- 'config/initializers/doorkeeper.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: short, verbose
|
||||
|
@ -252,44 +226,12 @@ Style/SignalException:
|
|||
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
|
||||
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Style/SingleArgumentDig:
|
||||
Exclude:
|
||||
- 'lib/webpacker/manifest_extensions.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: Mode.
|
||||
Style/StringConcatenation:
|
||||
Exclude:
|
||||
- 'config/initializers/paperclip.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
|
||||
# SupportedStyles: single_quotes, double_quotes
|
||||
Style/StringLiterals:
|
||||
Exclude:
|
||||
- 'config/environments/production.rb'
|
||||
- 'config/initializers/backtrace_silencers.rb'
|
||||
- 'config/initializers/http_client_proxy.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'config/initializers/webauthn.rb'
|
||||
- 'config/routes.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInArguments:
|
||||
Exclude:
|
||||
- 'config/initializers/paperclip.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
Exclude:
|
||||
- 'config/environments/production.rb'
|
||||
- 'config/environments/test.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: WordRegex.
|
||||
# SupportedStyles: percent, brackets
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# syntax=docker/dockerfile:1.4
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
# Please see https://docs.docker.com/engine/reference/builder for information about
|
||||
# the extended buildx capabilities used in this file.
|
||||
|
@ -205,7 +205,12 @@ ARG TARGETPLATFORM
|
|||
|
||||
RUN \
|
||||
# Use Ruby on Rails to create Mastodon assets
|
||||
OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \
|
||||
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=precompile_placeholder \
|
||||
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=precompile_placeholder \
|
||||
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=precompile_placeholder \
|
||||
OTP_SECRET=precompile_placeholder \
|
||||
SECRET_KEY_BASE=precompile_placeholder \
|
||||
bundle exec rails assets:precompile; \
|
||||
# Cleanup temporary files
|
||||
rm -fr /opt/mastodon/tmp;
|
||||
|
||||
|
|
40
Gemfile
40
Gemfile
|
@ -1,28 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
ruby '>= 3.0.0'
|
||||
ruby '>= 3.1.0'
|
||||
|
||||
gem 'puma', '~> 6.3'
|
||||
gem 'rails', '~> 7.1.1'
|
||||
gem 'propshaft'
|
||||
gem 'thor', '~> 1.2'
|
||||
gem 'puma', '~> 6.3'
|
||||
gem 'rack', '~> 2.2.7'
|
||||
gem 'rails', '~> 7.1.1'
|
||||
gem 'thor', '~> 1.2'
|
||||
|
||||
# For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182
|
||||
gem 'irb', '~> 1.8'
|
||||
|
||||
gem 'dotenv'
|
||||
gem 'haml-rails', '~>2.0'
|
||||
gem 'pg', '~> 1.5'
|
||||
gem 'pghero'
|
||||
gem 'dotenv-rails', '~> 2.8'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||
gem 'blurhash', '~> 0.1'
|
||||
gem 'fog-core', '<= 2.4.0'
|
||||
gem 'fog-openstack', '~> 1.0', require: false
|
||||
gem 'kt-paperclip', '~> 7.2'
|
||||
gem 'md-paperclip-azure', '~> 2.2', require: false
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
gem 'addressable', '~> 2.8'
|
||||
|
@ -39,11 +39,11 @@ end
|
|||
|
||||
gem 'net-ldap', '~> 0.18'
|
||||
|
||||
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
||||
gem 'omniauth-saml', '~> 2.0'
|
||||
gem 'omniauth_openid_connect', '~> 0.6.1'
|
||||
gem 'omniauth', '~> 2.0'
|
||||
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
||||
gem 'omniauth_openid_connect', '~> 0.6.1'
|
||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
||||
gem 'omniauth-saml', '~> 2.0'
|
||||
|
||||
gem 'color_diff', '~> 0.1'
|
||||
gem 'csv', '~> 3.2'
|
||||
|
@ -53,9 +53,8 @@ gem 'ed25519', '~> 1.3'
|
|||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'hiredis', '~> 0.6'
|
||||
gem 'redis-namespace', '~> 1.10'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 5.1'
|
||||
gem 'http', '~> 5.2.0'
|
||||
gem 'http_accept_language', '~> 2.1'
|
||||
gem 'httplog', '~> 1.6.2'
|
||||
gem 'i18n', '1.14.1' # TODO: Remove version when resolved: https://github.com/glebm/i18n-tasks/issues/552 / https://github.com/ruby-i18n/i18n/pull/688
|
||||
|
@ -63,40 +62,40 @@ gem 'idn-ruby', require: 'idn'
|
|||
gem 'inline_svg'
|
||||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
|
||||
gem 'nokogiri', '~> 1.15'
|
||||
gem 'nsa'
|
||||
gem 'oj', '~> 3.14'
|
||||
gem 'ox', '~> 2.14'
|
||||
gem 'parslet'
|
||||
gem 'posix-spawn'
|
||||
gem 'premailer-rails'
|
||||
gem 'public_suffix', '~> 5.0'
|
||||
gem 'pundit', '~> 2.3'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 6.6'
|
||||
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 7.0'
|
||||
gem 'redcarpet', '~> 3.6'
|
||||
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'redis-namespace', '~> 1.10'
|
||||
gem 'rqrcode', '~> 2.2'
|
||||
gem 'ruby-progressbar', '~> 1.13'
|
||||
gem 'sanitize', '~> 6.0'
|
||||
gem 'scenic', '~> 1.7'
|
||||
gem 'sidekiq', '~> 6.5'
|
||||
gem 'sidekiq-bulk', '~> 0.2.0'
|
||||
gem 'sidekiq-scheduler', '~> 5.0'
|
||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||
gem 'sidekiq-bulk', '~> 0.2.0'
|
||||
gem 'simple-navigation', '~> 4.4'
|
||||
gem 'simple_form', '~> 5.2'
|
||||
gem 'stoplight', '~> 3.0.1'
|
||||
gem 'simple-navigation', '~> 4.4'
|
||||
gem 'stoplight', '~> 4.1'
|
||||
gem 'strong_migrations', '1.8.0'
|
||||
gem 'tty-prompt', '~> 0.23', require: false
|
||||
gem 'twitter-text', '~> 3.1.0'
|
||||
gem 'tzinfo-data', '~> 1.2023'
|
||||
gem 'webauthn', '~> 3.0'
|
||||
gem 'webpacker', '~> 5.4'
|
||||
gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9'
|
||||
gem 'webauthn', '~> 3.0'
|
||||
|
||||
gem 'json-ld'
|
||||
gem 'json-ld-preloaded', '~> 3.2'
|
||||
|
@ -198,13 +197,14 @@ group :production do
|
|||
gem 'lograge', '~> 0.12'
|
||||
end
|
||||
|
||||
gem 'cocoon', '~> 1.2'
|
||||
gem 'concurrent-ruby', require: false
|
||||
gem 'connection_pool', require: false
|
||||
gem 'xorcist', '~> 1.1'
|
||||
|
||||
gem 'cocoon', '~> 1.2'
|
||||
|
||||
gem 'net-http', '~> 0.4.0'
|
||||
gem 'rubyzip', '~> 2.3'
|
||||
|
||||
gem 'hcaptcha', '~> 7.1'
|
||||
|
||||
gem 'mail', '~> 2.8'
|
||||
|
|
151
Gemfile.lock
151
Gemfile.lock
|
@ -99,20 +99,20 @@ GEM
|
|||
ast (2.4.2)
|
||||
attr_encrypted (4.0.0)
|
||||
encryptor (~> 3.0.0)
|
||||
attr_required (1.0.1)
|
||||
attr_required (1.0.2)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.873.0)
|
||||
aws-sdk-core (3.190.1)
|
||||
aws-partitions (1.916.0)
|
||||
aws-sdk-core (3.192.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.75.0)
|
||||
aws-sdk-core (~> 3, >= 3.188.0)
|
||||
aws-sdk-kms (1.79.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.142.0)
|
||||
aws-sdk-core (~> 3, >= 3.189.0)
|
||||
aws-sdk-s3 (1.147.0)
|
||||
aws-sdk-core (~> 3, >= 3.192.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (1.8.0)
|
||||
|
@ -132,7 +132,7 @@ GEM
|
|||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
rouge (>= 1.0.0)
|
||||
better_html (2.0.2)
|
||||
better_html (2.1.1)
|
||||
actionview (>= 6.0)
|
||||
activesupport (>= 6.0)
|
||||
ast (~> 2.0)
|
||||
|
@ -140,9 +140,9 @@ GEM
|
|||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (3.1.7)
|
||||
bindata (2.4.15)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bindata (2.5.0)
|
||||
binding_of_caller (1.0.1)
|
||||
debug_inspector (>= 1.2.0)
|
||||
blurhash (0.1.7)
|
||||
bootsnap (1.18.3)
|
||||
msgpack (~> 1.2)
|
||||
|
@ -167,7 +167,7 @@ GEM
|
|||
xpath (~> 3.2)
|
||||
case_transform (0.2)
|
||||
activesupport
|
||||
cbor (0.5.9.6)
|
||||
cbor (0.5.9.8)
|
||||
charlock_holmes (0.7.7)
|
||||
chewy (7.5.1)
|
||||
activesupport (>= 5.2)
|
||||
|
@ -182,23 +182,23 @@ GEM
|
|||
cose (1.3.0)
|
||||
cbor (~> 0.5.9)
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
crack (0.4.6)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.14.0)
|
||||
css_parser (1.17.1)
|
||||
addressable
|
||||
csv (3.2.8)
|
||||
csv (3.3.0)
|
||||
database_cleaner-active_record (2.1.0)
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
date (3.3.4)
|
||||
debug (1.9.1)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
debug_inspector (1.1.0)
|
||||
devise (4.9.3)
|
||||
debug_inspector (1.2.0)
|
||||
devise (4.9.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
|
@ -217,14 +217,10 @@ GEM
|
|||
discard (1.3.0)
|
||||
activerecord (>= 4.2, < 8)
|
||||
docile (1.4.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
doorkeeper (5.6.9)
|
||||
railties (>= 5)
|
||||
dotenv (2.8.1)
|
||||
dotenv-rails (2.8.1)
|
||||
dotenv (= 2.8.1)
|
||||
railties (>= 3.2)
|
||||
dotenv (3.1.0)
|
||||
drb (2.2.1)
|
||||
ed25519 (1.3.0)
|
||||
elasticsearch (7.13.3)
|
||||
|
@ -242,11 +238,11 @@ GEM
|
|||
mail (~> 2.7)
|
||||
encryptor (3.0.0)
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.7)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
excon (0.109.0)
|
||||
excon (0.110.0)
|
||||
fabrication (2.31.0)
|
||||
faker (3.2.3)
|
||||
faker (3.3.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
|
@ -274,10 +270,10 @@ GEM
|
|||
faraday_middleware (1.2.0)
|
||||
faraday (~> 1.0)
|
||||
fast_blank (1.0.1)
|
||||
fastimage (2.3.0)
|
||||
ffi (1.15.5)
|
||||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
fastimage (2.3.1)
|
||||
ffi (1.16.3)
|
||||
ffi-compiler (1.3.2)
|
||||
ffi (>= 1.15.5)
|
||||
rake
|
||||
fog-core (2.4.0)
|
||||
builder
|
||||
|
@ -291,7 +287,7 @@ GEM
|
|||
fog-core (~> 2.1)
|
||||
fog-json (>= 1.0)
|
||||
formatador (1.1.0)
|
||||
fugit (1.8.1)
|
||||
fugit (1.10.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
fuubar (2.5.1)
|
||||
|
@ -318,15 +314,16 @@ GEM
|
|||
hashie (5.0.0)
|
||||
hcaptcha (7.1.0)
|
||||
json
|
||||
highline (2.1.0)
|
||||
highline (3.0.1)
|
||||
hiredis (0.6.3)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http (5.1.1)
|
||||
http (5.2.0)
|
||||
addressable (~> 2.8)
|
||||
base64 (~> 0.1)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
llhttp-ffi (~> 0.4.0)
|
||||
llhttp-ffi (~> 0.5.0)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
|
@ -357,7 +354,7 @@ GEM
|
|||
rdoc
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.1)
|
||||
json (2.7.2)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.15.3.1)
|
||||
activesupport (>= 4.2)
|
||||
|
@ -374,7 +371,7 @@ GEM
|
|||
json-ld-preloaded (3.3.0)
|
||||
json-ld (~> 3.3)
|
||||
rdf (~> 3.3)
|
||||
json-schema (4.2.0)
|
||||
json-schema (4.3.0)
|
||||
addressable (>= 2.8)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.7.1)
|
||||
|
@ -399,15 +396,15 @@ GEM
|
|||
language_server-protocol (3.17.0.3)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (2.0.0)
|
||||
actionmailer (>= 5.2)
|
||||
letter_opener (~> 1.7)
|
||||
railties (>= 5.2)
|
||||
rexml
|
||||
link_header (0.0.8)
|
||||
llhttp-ffi (0.4.0)
|
||||
llhttp-ffi (0.5.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
lograge (0.14.0)
|
||||
|
@ -423,7 +420,7 @@ GEM
|
|||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
marcel (1.0.2)
|
||||
marcel (1.0.4)
|
||||
mario-redis-lock (1.2.1)
|
||||
redis (>= 3.0.5)
|
||||
matrix (0.4.2)
|
||||
|
@ -434,13 +431,13 @@ GEM
|
|||
memory_profiler (1.0.1)
|
||||
mime-types (3.5.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.1205)
|
||||
mime-types-data (3.2024.0305)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
mini_portile2 (2.8.6)
|
||||
minitest (5.22.3)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.3.0)
|
||||
multipart-post (2.4.0)
|
||||
mutex_m (0.2.0)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
|
@ -454,10 +451,10 @@ GEM
|
|||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.4.0.1)
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.16.3)
|
||||
nio4r (2.7.1)
|
||||
nokogiri (1.16.4)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nsa (0.3.0)
|
||||
|
@ -510,8 +507,7 @@ GEM
|
|||
pg (1.5.6)
|
||||
pghero (3.4.1)
|
||||
activerecord (>= 6)
|
||||
posix-spawn (0.3.15)
|
||||
premailer (1.21.0)
|
||||
premailer (1.23.0)
|
||||
addressable
|
||||
css_parser (>= 1.12.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
|
@ -527,7 +523,7 @@ GEM
|
|||
railties (>= 7.0.0)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (5.0.4)
|
||||
public_suffix (5.0.5)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.1)
|
||||
|
@ -548,7 +544,7 @@ GEM
|
|||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.6)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (1.0.2)
|
||||
rack (< 3)
|
||||
|
@ -594,7 +590,7 @@ GEM
|
|||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.1.0)
|
||||
rake (13.2.1)
|
||||
rdf (3.3.1)
|
||||
bcp47_spec (~> 0.2)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
|
@ -609,16 +605,16 @@ GEM
|
|||
redlock (1.3.2)
|
||||
redis (>= 3.0.0, < 6.0)
|
||||
regexp_parser (2.9.0)
|
||||
reline (0.4.3)
|
||||
reline (0.5.2)
|
||||
io-console (~> 0.5)
|
||||
request_store (1.5.1)
|
||||
request_store (1.6.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.2.6)
|
||||
rotp (6.3.0)
|
||||
rouge (4.1.2)
|
||||
rouge (4.2.1)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
|
@ -642,13 +638,13 @@ GEM
|
|||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-sidekiq (4.1.0)
|
||||
rspec-sidekiq (4.2.0)
|
||||
rspec-core (~> 3.0)
|
||||
rspec-expectations (~> 3.0)
|
||||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 8)
|
||||
rspec-support (3.13.1)
|
||||
rubocop (1.62.1)
|
||||
rubocop (1.63.3)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
|
@ -665,21 +661,24 @@ GEM
|
|||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.25.1)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-performance (1.20.2)
|
||||
rubocop-performance (1.21.0)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.30.0, < 2.0)
|
||||
rubocop-rails (2.24.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.24.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (2.27.1)
|
||||
rubocop-rspec (2.29.1)
|
||||
rubocop (~> 1.40)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
rubocop-rspec_rails (~> 2.28)
|
||||
rubocop-rspec_rails (2.28.3)
|
||||
rubocop (~> 1.40)
|
||||
ruby-prof (1.7.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-saml (1.15.0)
|
||||
ruby-saml (1.16.0)
|
||||
nokogiri (>= 1.13.10)
|
||||
rexml
|
||||
ruby2_keywords (0.0.5)
|
||||
|
@ -691,10 +690,10 @@ GEM
|
|||
sanitize (6.1.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
scenic (1.7.0)
|
||||
scenic (1.8.0)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
selenium-webdriver (4.18.1)
|
||||
selenium-webdriver (4.19.0)
|
||||
base64 (~> 0.2)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
|
@ -731,7 +730,7 @@ GEM
|
|||
smart_properties (1.17.0)
|
||||
stackprof (0.2.26)
|
||||
statsd-ruby (1.5.0)
|
||||
stoplight (3.0.2)
|
||||
stoplight (4.1.0)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.1.0)
|
||||
strong_migrations (1.8.0)
|
||||
|
@ -746,7 +745,7 @@ GEM
|
|||
unicode-display_width (>= 1.1.1, < 3)
|
||||
terrapin (1.0.1)
|
||||
climate_control
|
||||
test-prof (1.3.2)
|
||||
test-prof (1.3.3)
|
||||
thor (1.3.1)
|
||||
tilt (2.3.0)
|
||||
timeout (0.4.1)
|
||||
|
@ -763,7 +762,7 @@ GEM
|
|||
tty-cursor (~> 0.7)
|
||||
tty-screen (~> 0.8)
|
||||
wisper (~> 2.0)
|
||||
tty-screen (0.8.1)
|
||||
tty-screen (0.8.2)
|
||||
twitter-text (3.1.0)
|
||||
idn-ruby
|
||||
unf (~> 0.1.0)
|
||||
|
@ -773,9 +772,9 @@ GEM
|
|||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
unf_ext (0.0.9.1)
|
||||
unicode-display_width (2.5.0)
|
||||
uri (0.12.2)
|
||||
uri (0.13.0)
|
||||
validate_email (0.1.6)
|
||||
activemodel (>= 3.0)
|
||||
mail (>= 2.2.5)
|
||||
|
@ -796,7 +795,7 @@ GEM
|
|||
webfinger (1.2.0)
|
||||
activesupport
|
||||
httpclient (>= 2.4)
|
||||
webmock (3.22.0)
|
||||
webmock (3.23.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
@ -847,7 +846,7 @@ DEPENDENCIES
|
|||
devise_pam_authenticatable2 (~> 9.2)
|
||||
discard (~> 1.2)
|
||||
doorkeeper (~> 5.6)
|
||||
dotenv-rails (~> 2.8)
|
||||
dotenv
|
||||
ed25519 (~> 1.3)
|
||||
email_spec
|
||||
fabrication (~> 2.30)
|
||||
|
@ -862,7 +861,7 @@ DEPENDENCIES
|
|||
hcaptcha (~> 7.1)
|
||||
hiredis (~> 0.6)
|
||||
htmlentities (~> 4.3)
|
||||
http (~> 5.1)
|
||||
http (~> 5.2.0)
|
||||
http_accept_language (~> 2.1)
|
||||
httplog (~> 1.6.2)
|
||||
i18n (= 1.14.1)
|
||||
|
@ -879,6 +878,7 @@ DEPENDENCIES
|
|||
letter_opener_web (~> 2.0)
|
||||
link_header (~> 0.0)
|
||||
lograge (~> 0.12)
|
||||
mail (~> 2.8)
|
||||
mario-redis-lock (~> 1.2)
|
||||
md-paperclip-azure (~> 2.2)
|
||||
memory_profiler
|
||||
|
@ -897,7 +897,6 @@ DEPENDENCIES
|
|||
parslet
|
||||
pg (~> 1.5)
|
||||
pghero
|
||||
posix-spawn
|
||||
premailer-rails
|
||||
private_address_check (~> 0.5)
|
||||
propshaft
|
||||
|
@ -939,7 +938,7 @@ DEPENDENCIES
|
|||
simplecov (~> 0.22)
|
||||
simplecov-lcov (~> 0.8)
|
||||
stackprof
|
||||
stoplight (~> 3.0.1)
|
||||
stoplight (~> 4.1)
|
||||
strong_migrations (= 1.8.0)
|
||||
test-prof
|
||||
thor (~> 1.2)
|
||||
|
@ -953,7 +952,7 @@ DEPENDENCIES
|
|||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.2p53
|
||||
ruby 3.2.3p157
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.4
|
||||
2.5.9
|
||||
|
|
|
@ -112,7 +112,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
|
|||
|
||||
- **PostgreSQL** 12+
|
||||
- **Redis** 4+
|
||||
- **Ruby** 3.0+
|
||||
- **Ruby** 3.1+
|
||||
- **Node.js** 16+
|
||||
|
||||
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
|
||||
|
|
1
Vagrantfile
vendored
1
Vagrantfile
vendored
|
@ -173,6 +173,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
|
||||
# Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080
|
||||
config.vm.network :forwarded_port, guest: 3000, host: 3000
|
||||
config.vm.network :forwarded_port, guest: 3035, host: 3035
|
||||
config.vm.network :forwarded_port, guest: 4000, host: 4000
|
||||
config.vm.network :forwarded_port, guest: 8080, host: 8080
|
||||
config.vm.network :forwarded_port, guest: 9200, host: 9200
|
||||
|
|
|
@ -46,7 +46,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def default_statuses
|
||||
@account.statuses.not_local_only.where(visibility: [:public, :unlisted])
|
||||
@account.statuses.not_local_only.distributable_visibility
|
||||
end
|
||||
|
||||
def only_media_scope
|
||||
|
|
|
@ -31,7 +31,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
|
||||
def set_replies
|
||||
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses
|
||||
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
|
||||
@replies = @replies.distributable_visibility.where(in_reply_to_id: @status.id)
|
||||
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
|
||||
end
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ module Admin
|
|||
|
||||
layout 'admin'
|
||||
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
@ -19,10 +18,6 @@ module Admin
|
|||
@body_classes = 'admin'
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'admin'
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.cache_control.replace(private: true, no_store: true)
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ class Api::BaseController < ApplicationController
|
|||
include Api::CachingConcern
|
||||
include Api::ContentSecurityPolicy
|
||||
include Api::ErrorHandling
|
||||
include Api::Pagination
|
||||
|
||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||
|
||||
|
@ -29,21 +30,6 @@ class Api::BaseController < ApplicationController
|
|||
|
||||
protected
|
||||
|
||||
def pagination_max_id
|
||||
pagination_collection.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
pagination_collection.first.id
|
||||
end
|
||||
|
||||
def set_pagination_headers(next_path = nil, prev_path = nil)
|
||||
links = []
|
||||
links << [next_path, [%w(rel next)]] if next_path
|
||||
links << [prev_path, [%w(rel prev)]] if prev_path
|
||||
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
|
||||
end
|
||||
|
||||
def limit_param(default_limit)
|
||||
return default_limit unless params[:limit]
|
||||
|
||||
|
@ -72,10 +58,6 @@ class Api::BaseController < ApplicationController
|
|||
render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.unavailable?
|
||||
end
|
||||
|
||||
def require_valid_pagination_options!
|
||||
render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid?
|
||||
end
|
||||
|
||||
def require_user!
|
||||
if !current_user
|
||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||
|
@ -104,14 +86,6 @@ class Api::BaseController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_options_invalid?
|
||||
params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?)
|
||||
end
|
||||
|
||||
def respond_with_error(code)
|
||||
render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::CredentialsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:update]
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts', :'read:me' }, except: [:update]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update]
|
||||
before_action :require_user!
|
||||
|
||||
|
|
|
@ -12,10 +12,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
|||
private
|
||||
|
||||
def set_recently_used_tags
|
||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10)
|
||||
end
|
||||
|
||||
def featured_tag_ids
|
||||
current_account.featured_tags.pluck(:tag_id)
|
||||
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base
|
|||
end
|
||||
|
||||
def paginated_statuses
|
||||
Status.where(reblog_of_id: @status.id).where(visibility: [:public, :unlisted]).paginate_by_max_id(
|
||||
Status.where(reblog_of_id: @status.id).distributable_visibility.paginate_by_max_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id]
|
||||
|
|
|
@ -19,6 +19,7 @@ class ApplicationController < ActionController::Base
|
|||
helper_method :current_session
|
||||
helper_method :current_flavour
|
||||
helper_method :current_skin
|
||||
helper_method :current_theme
|
||||
helper_method :single_user_mode?
|
||||
helper_method :use_seamless_external_login?
|
||||
helper_method :omniauth_only?
|
||||
|
@ -164,10 +165,7 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
def respond_with_error(code)
|
||||
respond_to do |format|
|
||||
format.any do
|
||||
use_pack 'error'
|
||||
render "errors/#{code}", layout: 'error', status: code, formats: [:html]
|
||||
end
|
||||
format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] }
|
||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
|
||||
end
|
||||
end
|
||||
|
@ -176,10 +174,7 @@ class ApplicationController < ActionController::Base
|
|||
return unless self_destruct?
|
||||
|
||||
respond_to do |format|
|
||||
format.any do
|
||||
use_pack 'error'
|
||||
render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html]
|
||||
end
|
||||
format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] }
|
||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,6 @@ class Auth::ChallengesController < ApplicationController
|
|||
|
||||
layout 'auth'
|
||||
|
||||
before_action :set_pack
|
||||
before_action :authenticate_user!
|
||||
|
||||
skip_before_action :check_self_destruct!
|
||||
|
@ -21,10 +20,4 @@ class Auth::ChallengesController < ApplicationController
|
|||
render_challenge
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'auth'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
layout 'auth'
|
||||
|
||||
before_action :set_body_classes
|
||||
before_action :set_pack
|
||||
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
|
||||
before_action :redirect_confirmed_user, if: :signed_in_confirmed_user?
|
||||
|
||||
|
@ -66,10 +65,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
@confirmation_user.nil? || @confirmation_user.confirmed?
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'auth'
|
||||
end
|
||||
|
||||
def redirect_confirmed_user
|
||||
redirect_to(current_user.approved? ? root_path : edit_user_registration_path)
|
||||
end
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
class Auth::PasswordsController < Devise::PasswordsController
|
||||
skip_before_action :check_self_destruct!
|
||||
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
|
||||
layout 'auth'
|
||||
|
@ -32,8 +31,4 @@ class Auth::PasswordsController < Devise::PasswordsController
|
|||
def reset_password_token_is_valid?
|
||||
resource_class.with_reset_password_token(params[:reset_password_token]).present?
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'auth'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
before_action :set_invite, only: [:new, :create]
|
||||
before_action :check_enabled_registrations, only: [:new, :create]
|
||||
before_action :configure_sign_up_params, only: [:create]
|
||||
before_action :set_pack
|
||||
before_action :set_sessions, only: [:edit, :update]
|
||||
before_action :set_strikes, only: [:edit, :update]
|
||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||
|
@ -97,10 +96,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
|
||||
end
|
||||
|
|
|
@ -12,7 +12,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
skip_before_action :require_functional!
|
||||
skip_before_action :update_user_sign_in
|
||||
|
||||
prepend_before_action :set_pack
|
||||
prepend_before_action :check_suspicious!, only: [:create]
|
||||
|
||||
include Auth::TwoFactorAuthenticationConcern
|
||||
|
@ -104,10 +103,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'auth'
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'lighter'
|
||||
end
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
class Auth::SetupController < ApplicationController
|
||||
layout 'auth'
|
||||
|
||||
before_action :set_pack
|
||||
before_action :authenticate_user!
|
||||
before_action :require_unconfirmed_or_pending!
|
||||
before_action :set_body_classes
|
||||
|
@ -43,8 +42,4 @@ class Auth::SetupController < ApplicationController
|
|||
def user_params
|
||||
params.require(:user).permit(:email)
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'sign_up'
|
||||
end
|
||||
end
|
||||
|
|
36
app/controllers/concerns/api/pagination.rb
Normal file
36
app/controllers/concerns/api/pagination.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api::Pagination
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
protected
|
||||
|
||||
def pagination_max_id
|
||||
pagination_collection.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
pagination_collection.first.id
|
||||
end
|
||||
|
||||
def set_pagination_headers(next_path = nil, prev_path = nil)
|
||||
links = []
|
||||
links << [next_path, [%w(rel next)]] if next_path
|
||||
links << [prev_path, [%w(rel prev)]] if prev_path
|
||||
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
|
||||
end
|
||||
|
||||
def require_valid_pagination_options!
|
||||
render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_options_invalid?
|
||||
params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?)
|
||||
end
|
||||
end
|
|
@ -83,8 +83,6 @@ module Auth::TwoFactorAuthenticationConcern
|
|||
def prompt_for_two_factor(user)
|
||||
register_attempt_in_session(user)
|
||||
|
||||
use_pack 'auth'
|
||||
|
||||
@body_classes = 'lighter'
|
||||
@webauthn_enabled = user.webauthn_enabled?
|
||||
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
|
||||
|
|
|
@ -46,27 +46,19 @@ module CacheConcern
|
|||
end
|
||||
end
|
||||
|
||||
# TODO: Rename this method, as it does not perform any caching anymore.
|
||||
def cache_collection(raw, klass)
|
||||
return raw unless klass.respond_to?(:with_includes)
|
||||
return raw unless klass.respond_to?(:preload_cacheable_associations)
|
||||
|
||||
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
|
||||
return [] if raw.empty?
|
||||
records = raw.to_a
|
||||
|
||||
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
|
||||
klass.preload_cacheable_associations(records)
|
||||
|
||||
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
|
||||
|
||||
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
|
||||
|
||||
unless uncached_ids.empty?
|
||||
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
|
||||
Rails.cache.write_multi(uncached.values.to_h { |i| [i, i] })
|
||||
end
|
||||
|
||||
raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
|
||||
records
|
||||
end
|
||||
|
||||
# TODO: Rename this method, as it does not perform any caching anymore.
|
||||
def cache_collection_paginated_by_id(raw, klass, limit, options)
|
||||
cache_collection raw.cache_ids.to_a_paginated_by_id(limit, options), klass
|
||||
cache_collection raw.to_a_paginated_by_id(limit, options), klass
|
||||
end
|
||||
end
|
||||
|
|
|
@ -66,7 +66,7 @@ module SignatureVerification
|
|||
compare_signed_string = build_signed_string(include_query_string: false)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
|
||||
actor = stoplight_wrapper.run { actor_refresh_key!(actor) }
|
||||
|
||||
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
|
@ -226,10 +226,10 @@ module SignatureVerification
|
|||
end
|
||||
|
||||
if key_id.start_with?('acct:')
|
||||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
|
||||
stoplight_wrapper.run { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
|
||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
|
||||
account ||= stoplight_wrapper.run { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
|
||||
account
|
||||
end
|
||||
rescue Mastodon::PrivateNetworkAddressError => e
|
||||
|
@ -238,12 +238,11 @@ module SignatureVerification
|
|||
raise SignatureVerificationError, e.message
|
||||
end
|
||||
|
||||
def stoplight_wrap_request(&block)
|
||||
Stoplight("source:#{request.remote_ip}", &block)
|
||||
def stoplight_wrapper
|
||||
Stoplight("source:#{request.remote_ip}")
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
|
||||
.run
|
||||
end
|
||||
|
||||
def actor_refresh_key!(actor)
|
||||
|
|
|
@ -3,87 +3,22 @@
|
|||
module ThemingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def use_pack(pack_name)
|
||||
@core = resolve_pack_with_common(Themes.instance.core, pack_name)
|
||||
@theme = resolve_pack_with_common(Themes.instance.flavour(current_flavour), pack_name, current_skin)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_flavour
|
||||
[current_user&.setting_flavour, Setting.flavour, 'glitch', 'vanilla'].find { |flavour| Themes.instance.flavours.include?(flavour) }
|
||||
@current_flavour ||= [current_user&.setting_flavour, Setting.flavour, 'glitch', 'vanilla'].find { |flavour| Themes.instance.flavours.include?(flavour) }
|
||||
end
|
||||
|
||||
def current_skin
|
||||
@current_skin ||= begin
|
||||
skins = Themes.instance.skins_for(current_flavour)
|
||||
[current_user&.setting_skin, Setting.skin, 'default'].find { |skin| skins.include?(skin) }
|
||||
[current_user&.setting_skin, Setting.skin, 'system', 'default'].find { |skin| skins.include?(skin) }
|
||||
end
|
||||
end
|
||||
|
||||
def valid_pack_data?(data, pack_name)
|
||||
data['pack'].is_a?(Hash) && data['pack'][pack_name].present?
|
||||
end
|
||||
|
||||
def nil_pack(data)
|
||||
{
|
||||
use_common: true,
|
||||
flavour: data['name'],
|
||||
pack: nil,
|
||||
preload: nil,
|
||||
skin: nil,
|
||||
supported_locales: data['locales'],
|
||||
}
|
||||
end
|
||||
|
||||
def pack(data, pack_name, skin)
|
||||
pack_data = {
|
||||
use_common: true,
|
||||
flavour: data['name'],
|
||||
pack: pack_name,
|
||||
preload: nil,
|
||||
skin: nil,
|
||||
supported_locales: data['locales'],
|
||||
}
|
||||
|
||||
return pack_data unless data['pack'][pack_name].is_a?(Hash)
|
||||
|
||||
pack_data[:use_common] = false if data['pack'][pack_name]['use_common'] == false
|
||||
pack_data[:pack] = nil unless data['pack'][pack_name]['filename']
|
||||
|
||||
preloads = data['pack'][pack_name]['preload']
|
||||
pack_data[:preload] = [preloads] if preloads.is_a?(String)
|
||||
pack_data[:preload] = preloads if preloads.is_a?(Array)
|
||||
|
||||
if skin != 'default' && data['skin'][skin]
|
||||
pack_data[:skin] = skin if data['skin'][skin].include?(pack_name)
|
||||
elsif data['pack'][pack_name]['stylesheet']
|
||||
pack_data[:skin] = 'default'
|
||||
end
|
||||
|
||||
pack_data
|
||||
end
|
||||
|
||||
def resolve_pack(data, pack_name, skin)
|
||||
return pack(data, pack_name, skin) if valid_pack_data?(data, pack_name)
|
||||
return if data['name'].blank?
|
||||
|
||||
fallbacks = []
|
||||
if data.key?('fallback')
|
||||
fallbacks = data['fallback'] if data['fallback'].is_a?(Array)
|
||||
fallbacks = [data['fallback']] if data['fallback'].is_a?(String)
|
||||
elsif data['name'] != Setting.default_settings['flavour']
|
||||
fallbacks = [Setting.default_settings['flavour']]
|
||||
end
|
||||
|
||||
fallbacks.each do |fallback|
|
||||
return resolve_pack(Themes.instance.flavour(fallback), pack_name, skin) if Themes.instance.flavour(fallback)
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def resolve_pack_with_common(data, pack_name, skin = 'default')
|
||||
result = resolve_pack(data, pack_name, skin) || nil_pack(data)
|
||||
result[:common] = resolve_pack(data, 'common', skin) if result.delete(:use_common)
|
||||
result
|
||||
def current_theme
|
||||
# NOTE: this is slightly different from upstream, as it's a derived value used
|
||||
# for the sole purpose of pointing to the appropriate stylesheet pack
|
||||
[current_flavour, current_skin]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,6 @@ module WebAppControllerConcern
|
|||
vary_by 'Accept, Accept-Language, Cookie'
|
||||
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_pack
|
||||
before_action :set_app_body_class
|
||||
end
|
||||
|
||||
|
@ -37,8 +36,4 @@ module WebAppControllerConcern
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'home'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,15 +9,10 @@ class Disputes::BaseController < ApplicationController
|
|||
|
||||
before_action :set_body_classes
|
||||
before_action :authenticate_user!
|
||||
before_action :set_pack
|
||||
before_action :set_cache_headers
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'admin'
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
|
|
|
@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
before_action :set_filter
|
||||
before_action :set_status_filters
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
@ -27,10 +26,6 @@ class Filters::StatusesController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'admin'
|
||||
end
|
||||
|
||||
def set_filter
|
||||
@filter = current_account.custom_filters.find(params[:filter_id])
|
||||
end
|
||||
|
|
|
@ -5,7 +5,6 @@ class FiltersController < ApplicationController
|
|||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_filter, only: [:edit, :update, :destroy]
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
@ -45,10 +44,6 @@ class FiltersController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'settings'
|
||||
end
|
||||
|
||||
def set_filter
|
||||
@filter = current_account.custom_filters.find(params[:id])
|
||||
end
|
||||
|
|
|
@ -6,7 +6,6 @@ class InvitesController < ApplicationController
|
|||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
||||
|
@ -40,10 +39,6 @@ class InvitesController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'settings'
|
||||
end
|
||||
|
||||
def invites
|
||||
current_user.invites.order(id: :desc)
|
||||
end
|
||||
|
|
|
@ -10,7 +10,6 @@ class MediaController < ApplicationController
|
|||
before_action :verify_permitted_status!
|
||||
before_action :check_playable, only: :player
|
||||
before_action :allow_iframing, only: :player
|
||||
before_action :set_pack, only: :player
|
||||
|
||||
content_security_policy only: :player do |policy|
|
||||
policy.frame_ancestors(false)
|
||||
|
@ -48,8 +47,4 @@ class MediaController < ApplicationController
|
|||
def allow_iframing
|
||||
response.headers.delete('X-Frame-Options')
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'public'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
|
||||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
before_action :set_pack
|
||||
before_action :set_cache_headers
|
||||
|
||||
content_security_policy do |p|
|
||||
|
@ -20,10 +19,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
store_location_for(:user, request.url)
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'auth'
|
||||
end
|
||||
|
||||
def render_success
|
||||
if skip_authorization? || (matching_token? && !truthy_param?('force_login'))
|
||||
redirect_or_render authorize_response
|
||||
|
|
|
@ -5,7 +5,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||
|
||||
before_action :store_current_location
|
||||
before_action :authenticate_resource_owner!
|
||||
before_action :set_pack
|
||||
before_action :require_not_suspended!, only: :destroy
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
@ -31,10 +30,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||
store_location_for(:user, request.url)
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'settings'
|
||||
end
|
||||
|
||||
def require_not_suspended!
|
||||
forbidden if current_account.unavailable?
|
||||
end
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
class Redirect::BaseController < ApplicationController
|
||||
vary_by 'Accept-Language'
|
||||
|
||||
before_action :set_pack
|
||||
before_action :set_resource
|
||||
before_action :set_app_body_class
|
||||
|
||||
|
@ -22,8 +21,4 @@ class Redirect::BaseController < ApplicationController
|
|||
def set_resource
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'public'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,6 @@ class RelationshipsController < ApplicationController
|
|||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_accounts, only: :show
|
||||
before_action :set_pack
|
||||
before_action :set_relationships, only: :show
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
@ -73,10 +72,6 @@ class RelationshipsController < ApplicationController
|
|||
@body_classes = 'admin'
|
||||
end
|
||||
|
||||
def set_pack
|
||||
use_pack 'admin'
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.cache_control.replace(private: true, no_store: true)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::BaseController < ApplicationController
|
||||
before_action :set_pack
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
@ -10,10 +9,6 @@ class Settings::BaseController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'settings'
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
|
|
|
@ -38,7 +38,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
|||
end
|
||||
|
||||
def set_recently_used_tags
|
||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: @featured_tags.map(&:id)).limit(10)
|
||||
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
|
||||
end
|
||||
|
||||
def featured_tag_params
|
||||
|
|
|
@ -31,7 +31,7 @@ class Settings::ImportsController < Settings::BaseController
|
|||
def show; end
|
||||
|
||||
def failures
|
||||
@bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id])
|
||||
@bulk_import = current_account.bulk_imports.state_finished.find(params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
format.csv do
|
||||
|
@ -92,7 +92,7 @@ class Settings::ImportsController < Settings::BaseController
|
|||
end
|
||||
|
||||
def set_bulk_import
|
||||
@bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id])
|
||||
@bulk_import = current_account.bulk_imports.state_unconfirmed.find(params[:id])
|
||||
end
|
||||
|
||||
def set_recent_imports
|
||||
|
|
|
@ -7,10 +7,4 @@ class Settings::LoginActivitiesController < Settings::BaseController
|
|||
def index
|
||||
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'settings'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -85,10 +85,6 @@ module Settings
|
|||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'auth'
|
||||
end
|
||||
|
||||
def redirect_invalid_otp
|
||||
flash[:error] = t('webauthn_credentials.otp_required')
|
||||
redirect_to settings_two_factor_authentication_methods_path
|
||||
|
|
|
@ -4,17 +4,12 @@ class SharesController < ApplicationController
|
|||
layout 'modal'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_pack
|
||||
before_action :set_body_classes
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'share'
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'modal-layout compose-standalone'
|
||||
end
|
||||
|
|
|
@ -6,7 +6,6 @@ class StatusesCleanupController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
before_action :set_policy
|
||||
before_action :set_body_classes
|
||||
before_action :set_pack
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show; end
|
||||
|
@ -27,10 +26,6 @@ class StatusesCleanupController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def set_pack
|
||||
use_pack 'settings'
|
||||
end
|
||||
|
||||
def set_policy
|
||||
@policy = current_account.statuses_cleanup_policy || current_account.build_statuses_cleanup_policy(enabled: false)
|
||||
end
|
||||
|
|
|
@ -41,7 +41,6 @@ class StatusesController < ApplicationController
|
|||
end
|
||||
|
||||
def embed
|
||||
use_pack 'embed'
|
||||
return not_found if @status.hidden? || @status.reblog?
|
||||
|
||||
expires_in 180, public: true
|
||||
|
|
|
@ -233,6 +233,25 @@ module ApplicationHelper
|
|||
EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
|
||||
end
|
||||
|
||||
# glitch-soc addition to handle the multiple flavors
|
||||
def preload_locale_pack
|
||||
supported_locales = Themes.instance.flavour(current_flavour)['locales']
|
||||
preload_pack_asset "locales/#{current_flavour}/#{I18n.locale}-json.js" if supported_locales.include?(I18n.locale.to_s)
|
||||
end
|
||||
|
||||
def flavoured_javascript_pack_tag(pack_name, **options)
|
||||
javascript_pack_tag("flavours/#{current_flavour}/#{pack_name}", **options)
|
||||
end
|
||||
|
||||
def flavoured_stylesheet_pack_tag(pack_name, **options)
|
||||
stylesheet_pack_tag("flavours/#{current_flavour}/#{pack_name}", **options)
|
||||
end
|
||||
|
||||
def preload_signed_in_js_packs
|
||||
preload_files = Themes.instance.flavour(current_flavour)&.fetch('signed_in_preload', nil) || []
|
||||
safe_join(preload_files.map { |entry| preload_pack_asset entry })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def storage_host_var
|
||||
|
|
|
@ -19,6 +19,6 @@ module BrandingHelper
|
|||
end
|
||||
|
||||
def render_logo
|
||||
image_pack_tag('logo.svg', alt: 'Mastodon', class: 'logo logo--icon')
|
||||
image_tag(frontend_asset_path('images/logo.svg'), alt: 'Mastodon', class: 'logo logo--icon')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,13 +49,11 @@ module ContextHelper
|
|||
end
|
||||
|
||||
def serialized_context(named_contexts_map, context_extensions_map)
|
||||
context_array = []
|
||||
|
||||
named_contexts = named_contexts_map.keys
|
||||
context_extensions = context_extensions_map.keys
|
||||
|
||||
named_contexts.each do |key|
|
||||
context_array << NAMED_CONTEXT_MAP[key]
|
||||
context_array = named_contexts.map do |key|
|
||||
NAMED_CONTEXT_MAP[key]
|
||||
end
|
||||
|
||||
extensions = context_extensions.each_with_object({}) do |key, h|
|
||||
|
|
31
app/helpers/theme_helper.rb
Normal file
31
app/helpers/theme_helper.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ThemeHelper
|
||||
def theme_style_tags(flavour_and_skin)
|
||||
flavour, theme = flavour_and_skin
|
||||
|
||||
if theme == 'system'
|
||||
stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') +
|
||||
stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
||||
else
|
||||
stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous'
|
||||
end
|
||||
end
|
||||
|
||||
def theme_color_tags(flavour_and_skin)
|
||||
_, theme = flavour_and_skin
|
||||
|
||||
if theme == 'system'
|
||||
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') +
|
||||
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
|
||||
else
|
||||
tag.meta name: 'theme-color', content: theme_color_for(theme)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def theme_color_for(theme)
|
||||
theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark]
|
||||
end
|
||||
end
|
|
@ -1,340 +0,0 @@
|
|||
// This file will be loaded on admin pages, regardless of theme.
|
||||
|
||||
import 'packs/public-path';
|
||||
|
||||
import Rails from '@rails/ujs';
|
||||
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
|
||||
const valid = target.value && target.validity.valid;
|
||||
const element = document.querySelector<HTMLInputElement>(
|
||||
'input[type="datetime-local"]#announcement_ends_at',
|
||||
);
|
||||
|
||||
if (!element) return;
|
||||
|
||||
if (valid) {
|
||||
element.classList.remove('optional');
|
||||
element.required = true;
|
||||
element.min = target.value;
|
||||
} else {
|
||||
element.classList.add('optional');
|
||||
element.removeAttribute('required');
|
||||
element.removeAttribute('min');
|
||||
}
|
||||
};
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'input[type="datetime-local"]#announcement_starts_at',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLInputElement)
|
||||
setAnnouncementEndsAttributes(target);
|
||||
},
|
||||
);
|
||||
|
||||
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
||||
|
||||
const showSelectAll = () => {
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
selectAllMatchingElement?.classList.add('active');
|
||||
};
|
||||
|
||||
const hideSelectAll = () => {
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
const hiddenField = document.querySelector<HTMLInputElement>(
|
||||
'input#select_all_matching',
|
||||
);
|
||||
const selectedMsg = document.querySelector(
|
||||
'.batch-table__select-all .selected',
|
||||
);
|
||||
const notSelectedMsg = document.querySelector(
|
||||
'.batch-table__select-all .not-selected',
|
||||
);
|
||||
|
||||
selectAllMatchingElement?.classList.remove('active');
|
||||
selectedMsg?.classList.remove('active');
|
||||
notSelectedMsg?.classList.add('active');
|
||||
if (hiddenField) hiddenField.value = '0';
|
||||
};
|
||||
|
||||
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
|
||||
.forEach((content) => {
|
||||
content.checked = target.checked;
|
||||
});
|
||||
|
||||
if (selectAllMatchingElement) {
|
||||
if (target.checked) {
|
||||
showSelectAll();
|
||||
} else {
|
||||
hideSelectAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
|
||||
const hiddenField = document.querySelector<HTMLInputElement>(
|
||||
'#select_all_matching',
|
||||
);
|
||||
|
||||
if (!hiddenField) return;
|
||||
|
||||
const active = hiddenField.value === '1';
|
||||
const selectedMsg = document.querySelector(
|
||||
'.batch-table__select-all .selected',
|
||||
);
|
||||
const notSelectedMsg = document.querySelector(
|
||||
'.batch-table__select-all .not-selected',
|
||||
);
|
||||
|
||||
if (!selectedMsg || !notSelectedMsg) return;
|
||||
|
||||
if (active) {
|
||||
hiddenField.value = '0';
|
||||
selectedMsg.classList.remove('active');
|
||||
notSelectedMsg.classList.add('active');
|
||||
} else {
|
||||
hiddenField.value = '1';
|
||||
notSelectedMsg.classList.remove('active');
|
||||
selectedMsg.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
|
||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
|
||||
if (checkAllElement) {
|
||||
const allCheckboxes = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
|
||||
);
|
||||
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
|
||||
checkAllElement.indeterminate =
|
||||
!checkAllElement.checked &&
|
||||
allCheckboxes.some((content) => content.checked);
|
||||
|
||||
if (selectAllMatchingElement) {
|
||||
if (checkAllElement.checked) {
|
||||
showSelectAll();
|
||||
} else {
|
||||
hideSelectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'.filter-subset--with-select select',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) target.form?.submit();
|
||||
},
|
||||
);
|
||||
|
||||
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
|
||||
const rejectMediaDiv = document.querySelector(
|
||||
'.input.with_label.domain_block_reject_media',
|
||||
);
|
||||
const rejectReportsDiv = document.querySelector(
|
||||
'.input.with_label.domain_block_reject_reports',
|
||||
);
|
||||
|
||||
if (rejectMediaDiv && rejectMediaDiv instanceof HTMLElement) {
|
||||
rejectMediaDiv.style.display =
|
||||
target.value === 'suspend' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
if (rejectReportsDiv && rejectReportsDiv instanceof HTMLElement) {
|
||||
rejectReportsDiv.style.display =
|
||||
target.value === 'suspend' ? 'none' : 'block';
|
||||
}
|
||||
};
|
||||
|
||||
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
|
||||
});
|
||||
|
||||
const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
|
||||
const bootstrapTimelineAccountsField =
|
||||
document.querySelector<HTMLInputElement>(
|
||||
'#form_admin_settings_bootstrap_timeline_accounts',
|
||||
);
|
||||
|
||||
if (bootstrapTimelineAccountsField) {
|
||||
bootstrapTimelineAccountsField.disabled = !target.checked;
|
||||
if (target.checked) {
|
||||
bootstrapTimelineAccountsField.parentElement?.classList.remove(
|
||||
'disabled',
|
||||
);
|
||||
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.remove(
|
||||
'disabled',
|
||||
);
|
||||
} else {
|
||||
bootstrapTimelineAccountsField.parentElement?.classList.add('disabled');
|
||||
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.add(
|
||||
'disabled',
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#form_admin_settings_enable_bootstrap_timeline_accounts',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLInputElement)
|
||||
onEnableBootstrapTimelineAccountsChange(target);
|
||||
},
|
||||
);
|
||||
|
||||
const onChangeRegistrationMode = (target: HTMLSelectElement) => {
|
||||
const enabled = target.value === 'approved';
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLElement>(
|
||||
'.form_admin_settings_registrations_mode .warning-hint',
|
||||
)
|
||||
.forEach((warning_hint) => {
|
||||
warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>(
|
||||
'input#form_admin_settings_require_invite_text',
|
||||
)
|
||||
.forEach((input) => {
|
||||
input.disabled = !enabled;
|
||||
if (enabled) {
|
||||
let element: HTMLElement | null = input;
|
||||
do {
|
||||
element.classList.remove('disabled');
|
||||
element = element.parentElement;
|
||||
} while (element && !element.classList.contains('fields-group'));
|
||||
} else {
|
||||
let element: HTMLElement | null = input;
|
||||
do {
|
||||
element.classList.add('disabled');
|
||||
element = element.parentElement;
|
||||
} while (element && !element.classList.contains('fields-group'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const convertUTCDateTimeToLocal = (value: string) => {
|
||||
const date = new Date(value + 'Z');
|
||||
const twoChars = (x: number) => x.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
function convertLocalDatetimeToUTC(value: string) {
|
||||
const date = new Date(value);
|
||||
const fullISO8601 = date.toISOString();
|
||||
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
||||
}
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#form_admin_settings_registrations_mode',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
|
||||
},
|
||||
);
|
||||
|
||||
ready(() => {
|
||||
const domainBlockSeveritySelect = document.querySelector<HTMLSelectElement>(
|
||||
'select#domain_block_severity',
|
||||
);
|
||||
if (domainBlockSeveritySelect)
|
||||
onDomainBlockSeverityChange(domainBlockSeveritySelect);
|
||||
|
||||
const enableBootstrapTimelineAccounts =
|
||||
document.querySelector<HTMLInputElement>(
|
||||
'input#form_admin_settings_enable_bootstrap_timeline_accounts',
|
||||
);
|
||||
if (enableBootstrapTimelineAccounts)
|
||||
onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
|
||||
|
||||
const registrationMode = document.querySelector<HTMLSelectElement>(
|
||||
'select#form_admin_settings_registrations_mode',
|
||||
);
|
||||
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
||||
|
||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
if (checkAllElement) {
|
||||
const allCheckboxes = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
|
||||
);
|
||||
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
|
||||
checkAllElement.indeterminate =
|
||||
!checkAllElement.checked &&
|
||||
allCheckboxes.some((content) => content.checked);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector('a#add-instance-button')
|
||||
?.addEventListener('click', (e) => {
|
||||
const domain = document.querySelector<HTMLInputElement>(
|
||||
'input[type="text"]#by_domain',
|
||||
)?.value;
|
||||
|
||||
if (domain && e.target instanceof HTMLAnchorElement) {
|
||||
const url = new URL(e.target.href);
|
||||
url.searchParams.set('_domain', domain);
|
||||
e.target.href = url.toString();
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
||||
.forEach((element) => {
|
||||
if (element.value) {
|
||||
element.value = convertUTCDateTimeToLocal(element.value);
|
||||
}
|
||||
if (element.placeholder) {
|
||||
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
|
||||
}
|
||||
});
|
||||
|
||||
Rails.delegate(document, 'form', 'submit', ({ target }) => {
|
||||
if (target instanceof HTMLFormElement)
|
||||
target
|
||||
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
||||
.forEach((element) => {
|
||||
if (element.value && element.validity.valid) {
|
||||
element.value = convertLocalDatetimeToUTC(element.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const announcementStartsAt = document.querySelector<HTMLInputElement>(
|
||||
'input[type="datetime-local"]#announcement_starts_at',
|
||||
);
|
||||
if (announcementStartsAt) {
|
||||
setAnnouncementEndsAttributes(announcementStartsAt);
|
||||
}
|
||||
}).catch((reason) => {
|
||||
throw reason;
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
import 'packs/public-path';
|
||||
import './settings';
|
||||
import './two_factor_authentication';
|
|
@ -1,6 +0,0 @@
|
|||
// This file will be loaded on all pages, regardless of theme.
|
||||
|
||||
import 'packs/public-path';
|
||||
import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
require.context('../images/', true);
|
|
@ -1,41 +0,0 @@
|
|||
// This file will be loaded on embed pages, regardless of theme.
|
||||
|
||||
import 'packs/public-path';
|
||||
import ready from '../mastodon/ready';
|
||||
|
||||
interface SetHeightMessage {
|
||||
type: 'setHeight';
|
||||
id: string;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
|
||||
if (
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'type' in data &&
|
||||
data.type === 'setHeight'
|
||||
)
|
||||
return true;
|
||||
else return false;
|
||||
}
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
|
||||
if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
|
||||
|
||||
const data = e.data;
|
||||
|
||||
ready(() => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: 'setHeight',
|
||||
id: data.id,
|
||||
height: document.getElementsByTagName('html')[0].scrollHeight,
|
||||
},
|
||||
'*',
|
||||
);
|
||||
}).catch((e) => {
|
||||
console.error('Error in setHeightMessage postMessage', e);
|
||||
});
|
||||
});
|
|
@ -1,70 +0,0 @@
|
|||
// This file will be loaded on settings pages, regardless of theme.
|
||||
|
||||
import 'packs/public-path';
|
||||
import Rails from '@rails/ujs';
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#edit_profile input[type=file]',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
const avatar = document.querySelector<HTMLImageElement>(
|
||||
`img#${target.id}-preview`,
|
||||
);
|
||||
|
||||
if (!avatar) return;
|
||||
|
||||
let file: File | undefined;
|
||||
if (target.files) file = target.files[0];
|
||||
|
||||
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
|
||||
|
||||
if (url) avatar.src = url;
|
||||
},
|
||||
);
|
||||
|
||||
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
target.focus();
|
||||
target.select();
|
||||
target.setSelectionRange(0, target.value.length);
|
||||
});
|
||||
|
||||
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
||||
if (!(target instanceof HTMLButtonElement)) return;
|
||||
|
||||
const input = target.parentNode?.querySelector<HTMLInputElement>(
|
||||
'.input-copy__wrapper input',
|
||||
);
|
||||
|
||||
if (!input) return;
|
||||
|
||||
const oldReadOnly = input.readOnly;
|
||||
|
||||
input.readOnly = false;
|
||||
input.focus();
|
||||
input.select();
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
input.blur();
|
||||
|
||||
const parent = target.parentElement;
|
||||
|
||||
if (!parent) return;
|
||||
parent.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
parent.classList.remove('copied');
|
||||
}, 700);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
input.readOnly = oldReadOnly;
|
||||
});
|
|
@ -1,24 +0,0 @@
|
|||
# These packs will be loaded on every appropriate page, regardless of
|
||||
# theme.
|
||||
pack:
|
||||
about:
|
||||
admin: admin.ts
|
||||
auth: auth.js
|
||||
common:
|
||||
filename: common.js
|
||||
stylesheet: true
|
||||
embed: embed.ts
|
||||
error:
|
||||
home:
|
||||
inert:
|
||||
filename: inert.js
|
||||
stylesheet: true
|
||||
mailer:
|
||||
filename: mailer.js
|
||||
stylesheet: true
|
||||
modal:
|
||||
public:
|
||||
settings: settings.ts
|
||||
sign_up:
|
||||
share:
|
||||
remote_interaction_helper: remote_interaction_helper.ts
|
|
@ -1,32 +0,0 @@
|
|||
import { openModal } from './modal';
|
||||
|
||||
export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL';
|
||||
export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY';
|
||||
|
||||
export function initBoostModal(props) {
|
||||
return (dispatch, getState) => {
|
||||
const default_privacy = getState().getIn(['compose', 'default_privacy']);
|
||||
|
||||
const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy;
|
||||
|
||||
dispatch({
|
||||
type: BOOSTS_INIT_MODAL,
|
||||
privacy,
|
||||
});
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'BOOST',
|
||||
modalProps: props,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function changeBoostPrivacy(privacy) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: BOOSTS_CHANGE_PRIVACY,
|
||||
privacy,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,152 +0,0 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import api from '../api';
|
||||
import { compareId } from '../compare_id';
|
||||
|
||||
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
|
||||
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
|
||||
export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL';
|
||||
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
|
||||
|
||||
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
||||
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||
const params = _buildParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
// The Fetch API allows us to perform requests that will be carried out
|
||||
// after the page closes. But that only works if the `keepalive` attribute
|
||||
// is supported.
|
||||
if (window.fetch && 'keepalive' in new Request('')) {
|
||||
fetch('/api/v1/markers', {
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (navigator && navigator.sendBeacon) {
|
||||
// Failing that, we can use sendBeacon, but we have to encode the data as
|
||||
// FormData for DoorKeeper to recognize the token.
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('bearer_token', accessToken);
|
||||
|
||||
for (const [id, value] of Object.entries(params)) {
|
||||
formData.append(`${id}[last_read_id]`, value.last_read_id);
|
||||
}
|
||||
|
||||
if (navigator.sendBeacon('/api/v1/markers', formData)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
|
||||
// request.
|
||||
try {
|
||||
const client = new XMLHttpRequest();
|
||||
|
||||
client.open('POST', '/api/v1/markers', false);
|
||||
client.setRequestHeader('Content-Type', 'application/json');
|
||||
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
client.send(JSON.stringify(params));
|
||||
} catch (e) {
|
||||
// Do not make the BeforeUnload handler error out
|
||||
}
|
||||
};
|
||||
|
||||
const _buildParams = (state) => {
|
||||
const params = {};
|
||||
|
||||
const lastHomeId = state.getIn(['timelines', 'home', 'items'], ImmutableList()).find(item => item !== null);
|
||||
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
|
||||
|
||||
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
||||
params.home = {
|
||||
last_read_id: lastHomeId,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastNotificationId && lastNotificationId !== '0' && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
|
||||
params.notifications = {
|
||||
last_read_id: lastNotificationId,
|
||||
};
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
|
||||
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||
const params = _buildParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
api(getState).post('/api/v1/markers', params).then(() => {
|
||||
dispatch(submitMarkersSuccess(params));
|
||||
}).catch(() => {});
|
||||
}, 300000, { leading: true, trailing: true });
|
||||
|
||||
export function submitMarkersSuccess({ home, notifications }) {
|
||||
return {
|
||||
type: MARKERS_SUBMIT_SUCCESS,
|
||||
home: (home || {}).last_read_id,
|
||||
notifications: (notifications || {}).last_read_id,
|
||||
};
|
||||
}
|
||||
|
||||
export function submitMarkers(params = {}) {
|
||||
const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
|
||||
|
||||
if (params.immediate === true) {
|
||||
debouncedSubmitMarkers.flush();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const fetchMarkers = () => (dispatch, getState) => {
|
||||
const params = { timeline: ['notifications'] };
|
||||
|
||||
dispatch(fetchMarkersRequest());
|
||||
|
||||
api(getState).get('/api/v1/markers', { params }).then(response => {
|
||||
dispatch(fetchMarkersSuccess(response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchMarkersFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
export function fetchMarkersRequest() {
|
||||
return {
|
||||
type: MARKERS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMarkersSuccess(markers) {
|
||||
return {
|
||||
type: MARKERS_FETCH_SUCCESS,
|
||||
markers,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchMarkersFail(error) {
|
||||
return {
|
||||
type: MARKERS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
};
|
||||
}
|
166
app/javascript/flavours/glitch/actions/markers.ts
Normal file
166
app/javascript/flavours/glitch/actions/markers.ts
Normal file
|
@ -0,0 +1,166 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import type { MarkerJSON } from 'flavours/glitch/api_types/markers';
|
||||
import type { RootState } from 'flavours/glitch/store';
|
||||
import { createAppAsyncThunk } from 'flavours/glitch/store/typed_functions';
|
||||
|
||||
import api, { authorizationTokenFromState } from '../api';
|
||||
import { compareId } from '../compare_id';
|
||||
|
||||
export const synchronouslySubmitMarkers = createAppAsyncThunk(
|
||||
'markers/submit',
|
||||
async (_args, { getState }) => {
|
||||
const accessToken = authorizationTokenFromState(getState);
|
||||
const params = buildPostMarkersParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || !accessToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The Fetch API allows us to perform requests that will be carried out
|
||||
// after the page closes. But that only works if the `keepalive` attribute
|
||||
// is supported.
|
||||
if ('fetch' in window && 'keepalive' in new Request('')) {
|
||||
await fetch('/api/v1/markers', {
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
|
||||
return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} else if ('navigator' && 'sendBeacon' in navigator) {
|
||||
// Failing that, we can use sendBeacon, but we have to encode the data as
|
||||
// FormData for DoorKeeper to recognize the token.
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('bearer_token', accessToken);
|
||||
|
||||
for (const [id, value] of Object.entries(params)) {
|
||||
if (value.last_read_id)
|
||||
formData.append(`${id}[last_read_id]`, value.last_read_id);
|
||||
}
|
||||
|
||||
if (navigator.sendBeacon('/api/v1/markers', formData)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
|
||||
// request.
|
||||
try {
|
||||
const client = new XMLHttpRequest();
|
||||
|
||||
client.open('POST', '/api/v1/markers', false);
|
||||
client.setRequestHeader('Content-Type', 'application/json');
|
||||
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
client.send(JSON.stringify(params));
|
||||
} catch (e) {
|
||||
// Do not make the BeforeUnload handler error out
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
interface MarkerParam {
|
||||
last_read_id?: string;
|
||||
}
|
||||
|
||||
function getLastHomeId(state: RootState): string | undefined {
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
||||
return (
|
||||
state
|
||||
// @ts-expect-error state.timelines is not yet typed
|
||||
.getIn(['timelines', 'home', 'items'], ImmutableList())
|
||||
// @ts-expect-error state.timelines is not yet typed
|
||||
.find((item) => item !== null)
|
||||
);
|
||||
}
|
||||
|
||||
function getLastNotificationId(state: RootState): string | undefined {
|
||||
// @ts-expect-error state.notifications is not yet typed
|
||||
return state.getIn(['notifications', 'lastReadId']);
|
||||
}
|
||||
|
||||
const buildPostMarkersParams = (state: RootState) => {
|
||||
const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
|
||||
|
||||
const lastHomeId = getLastHomeId(state);
|
||||
const lastNotificationId = getLastNotificationId(state);
|
||||
|
||||
if (lastHomeId && compareId(lastHomeId, state.markers.home) > 0) {
|
||||
params.home = {
|
||||
last_read_id: lastHomeId,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
lastNotificationId &&
|
||||
lastNotificationId !== '0' &&
|
||||
compareId(lastNotificationId, state.markers.notifications) > 0
|
||||
) {
|
||||
params.notifications = {
|
||||
last_read_id: lastNotificationId,
|
||||
};
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
export const submitMarkersAction = createAppAsyncThunk<{
|
||||
home: string | undefined;
|
||||
notifications: string | undefined;
|
||||
}>('markers/submitAction', async (_args, { getState }) => {
|
||||
const accessToken = authorizationTokenFromState(getState);
|
||||
const params = buildPostMarkersParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0 || accessToken === '') {
|
||||
return { home: undefined, notifications: undefined };
|
||||
}
|
||||
|
||||
await api(getState).post<MarkerJSON>('/api/v1/markers', params);
|
||||
|
||||
return {
|
||||
home: params.home?.last_read_id,
|
||||
notifications: params.notifications?.last_read_id,
|
||||
};
|
||||
});
|
||||
|
||||
const debouncedSubmitMarkers = debounce(
|
||||
(dispatch) => {
|
||||
dispatch(submitMarkersAction());
|
||||
},
|
||||
300000,
|
||||
{
|
||||
leading: true,
|
||||
trailing: true,
|
||||
},
|
||||
);
|
||||
|
||||
export const submitMarkers = createAppAsyncThunk(
|
||||
'markers/submit',
|
||||
(params: { immediate?: boolean }, { dispatch }) => {
|
||||
debouncedSubmitMarkers(dispatch);
|
||||
|
||||
if (params.immediate) {
|
||||
debouncedSubmitMarkers.flush();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchMarkers = createAppAsyncThunk(
|
||||
'markers/fetch',
|
||||
async (_args, { getState }) => {
|
||||
const response = await api(getState).get<Record<string, MarkerJSON>>(
|
||||
`/api/v1/markers`,
|
||||
{ params: { timeline: ['notifications'] } },
|
||||
);
|
||||
|
||||
return { markers: response.data };
|
||||
},
|
||||
);
|
|
@ -1,46 +0,0 @@
|
|||
// @ts-check
|
||||
|
||||
export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY';
|
||||
export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
|
||||
|
||||
/**
|
||||
* @typedef MediaProps
|
||||
* @property {string} src
|
||||
* @property {boolean} muted
|
||||
* @property {number} volume
|
||||
* @property {number} currentTime
|
||||
* @property {string} poster
|
||||
* @property {string} backgroundColor
|
||||
* @property {string} foregroundColor
|
||||
* @property {string} accentColor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} statusId
|
||||
* @param {string} accountId
|
||||
* @param {string} playerType
|
||||
* @param {MediaProps} props
|
||||
* @returns {object}
|
||||
*/
|
||||
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
|
||||
// @ts-expect-error
|
||||
return (dispatch, getState) => {
|
||||
// Do not open a player for a toot that does not exist
|
||||
if (getState().hasIn(['statuses', statusId])) {
|
||||
dispatch({
|
||||
type: PICTURE_IN_PICTURE_DEPLOY,
|
||||
statusId,
|
||||
accountId,
|
||||
playerType,
|
||||
props,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* @return {object}
|
||||
*/
|
||||
export const removePictureInPicture = () => ({
|
||||
type: PICTURE_IN_PICTURE_REMOVE,
|
||||
});
|
31
app/javascript/flavours/glitch/actions/picture_in_picture.ts
Normal file
31
app/javascript/flavours/glitch/actions/picture_in_picture.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import type { PIPMediaProps } from 'flavours/glitch/reducers/picture_in_picture';
|
||||
import { createAppAsyncThunk } from 'flavours/glitch/store/typed_functions';
|
||||
|
||||
interface DeployParams {
|
||||
statusId: string;
|
||||
accountId: string;
|
||||
playerType: 'audio' | 'video';
|
||||
props: PIPMediaProps;
|
||||
}
|
||||
|
||||
export const removePictureInPicture = createAction('pip/remove');
|
||||
|
||||
export const deployPictureInPictureAction =
|
||||
createAction<DeployParams>('pip/deploy');
|
||||
|
||||
export const deployPictureInPicture = createAppAsyncThunk(
|
||||
'pip/deploy',
|
||||
(args: DeployParams, { dispatch, getState }) => {
|
||||
const { statusId } = args;
|
||||
|
||||
// Do not open a player for a toot that does not exist
|
||||
|
||||
// @ts-expect-error state.statuses is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
if (getState().hasIn(['statuses', statusId])) {
|
||||
dispatch(deployPictureInPictureAction(args));
|
||||
}
|
||||
},
|
||||
);
|
|
@ -29,9 +29,14 @@ const setCSRFHeader = () => {
|
|||
|
||||
void ready(setCSRFHeader);
|
||||
|
||||
export const authorizationTokenFromState = (getState?: GetState) => {
|
||||
return (
|
||||
getState && (getState().meta.get('access_token', '') as string | false)
|
||||
);
|
||||
};
|
||||
|
||||
const authorizationHeaderFromState = (getState?: GetState) => {
|
||||
const accessToken =
|
||||
getState && (getState().meta.get('access_token', '') as string);
|
||||
const accessToken = authorizationTokenFromState(getState);
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
|
|
7
app/javascript/flavours/glitch/api_types/markers.ts
Normal file
7
app/javascript/flavours/glitch/api_types/markers.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
// See app/serializers/rest/account_serializer.rb
|
||||
|
||||
export interface MarkerJSON {
|
||||
last_read_id: string;
|
||||
version: string;
|
||||
updated_at: string;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// See app/serializers/rest/media_attachment_serializer.rb
|
||||
|
||||
export type MediaAttachmentType =
|
||||
| 'image'
|
||||
| 'gifv'
|
||||
| 'video'
|
||||
| 'unknown'
|
||||
| 'audio';
|
||||
|
||||
export interface ApiMediaAttachmentJSON {
|
||||
id: string;
|
||||
type: MediaAttachmentType;
|
||||
url: string;
|
||||
preview_url: string;
|
||||
remoteUrl: string;
|
||||
preview_remote_url: string;
|
||||
text_url: string;
|
||||
// TODO: how to define this?
|
||||
meta: unknown;
|
||||
description?: string;
|
||||
blurhash: string;
|
||||
}
|
23
app/javascript/flavours/glitch/api_types/polls.ts
Normal file
23
app/javascript/flavours/glitch/api_types/polls.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||
|
||||
// See app/serializers/rest/poll_serializer.rb
|
||||
|
||||
export interface ApiPollOptionJSON {
|
||||
title: string;
|
||||
votes_count: number;
|
||||
}
|
||||
|
||||
export interface ApiPollJSON {
|
||||
id: string;
|
||||
expires_at: string;
|
||||
expired: boolean;
|
||||
multiple: boolean;
|
||||
votes_count: number;
|
||||
voters_count: number;
|
||||
|
||||
options: ApiPollOptionJSON[];
|
||||
emojis: ApiCustomEmojiJSON[];
|
||||
|
||||
voted: boolean;
|
||||
own_votes: number[];
|
||||
}
|
95
app/javascript/flavours/glitch/api_types/statuses.ts
Normal file
95
app/javascript/flavours/glitch/api_types/statuses.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
// See app/serializers/rest/status_serializer.rb
|
||||
|
||||
import type { ApiAccountJSON } from './accounts';
|
||||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||
import type { ApiMediaAttachmentJSON } from './media_attachments';
|
||||
import type { ApiPollJSON } from './polls';
|
||||
|
||||
// See app/modals/status.rb
|
||||
export type StatusVisibility =
|
||||
| 'public'
|
||||
| 'unlisted'
|
||||
| 'private'
|
||||
// | 'limited' // This is never exposed to the API (they become `private`)
|
||||
| 'direct';
|
||||
|
||||
export interface ApiStatusApplicationJSON {
|
||||
name: string;
|
||||
website: string;
|
||||
}
|
||||
|
||||
export interface ApiTagJSON {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ApiMentionJSON {
|
||||
id: string;
|
||||
username: string;
|
||||
url: string;
|
||||
acct: string;
|
||||
}
|
||||
|
||||
export interface ApiPreviewCardJSON {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
language: string;
|
||||
type: string;
|
||||
author_name: string;
|
||||
author_url: string;
|
||||
provider_name: string;
|
||||
provider_url: string;
|
||||
html: string;
|
||||
width: number;
|
||||
height: number;
|
||||
image: string;
|
||||
image_description: string;
|
||||
embed_url: string;
|
||||
blurhash: string;
|
||||
published_at: string;
|
||||
}
|
||||
|
||||
export interface ApiStatusJSON {
|
||||
id: string;
|
||||
created_at: string;
|
||||
in_reply_to_id?: string;
|
||||
in_reply_to_account_id?: string;
|
||||
sensitive: boolean;
|
||||
spoiler_text?: string;
|
||||
visibility: StatusVisibility;
|
||||
language: string;
|
||||
uri: string;
|
||||
url: string;
|
||||
replies_count: number;
|
||||
reblogs_count: number;
|
||||
favorites_count: number;
|
||||
edited_at?: string;
|
||||
|
||||
favorited?: boolean;
|
||||
reblogged?: boolean;
|
||||
muted?: boolean;
|
||||
bookmarked?: boolean;
|
||||
pinned?: boolean;
|
||||
|
||||
// filtered: FilterResult[]
|
||||
filtered: unknown; // TODO
|
||||
content?: string;
|
||||
text?: string;
|
||||
|
||||
reblog?: ApiStatusJSON;
|
||||
application?: ApiStatusApplicationJSON;
|
||||
account: ApiAccountJSON;
|
||||
media_attachments: ApiMediaAttachmentJSON[];
|
||||
mentions: ApiMentionJSON[];
|
||||
|
||||
tags: ApiTagJSON[];
|
||||
emojis: ApiCustomEmojiJSON[];
|
||||
|
||||
card?: ApiPreviewCardJSON;
|
||||
poll?: ApiPollJSON;
|
||||
|
||||
// glitch-soc additions
|
||||
local_only?: boolean;
|
||||
content_type?: string;
|
||||
}
|
12
app/javascript/flavours/glitch/common.js
Normal file
12
app/javascript/flavours/glitch/common.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import Rails from '@rails/ujs';
|
||||
import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
export function start() {
|
||||
require.context('@/images/', true, /\.(jpg|png|svg)$/);
|
||||
|
||||
try {
|
||||
Rails.start();
|
||||
} catch (e) {
|
||||
// If called twice
|
||||
}
|
||||
}
|
|
@ -1,26 +1,26 @@
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface BaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
interface BaseProps
|
||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||
block?: boolean;
|
||||
secondary?: boolean;
|
||||
text?: JSX.Element;
|
||||
}
|
||||
|
||||
interface PropsWithChildren extends BaseProps {
|
||||
text?: never;
|
||||
interface PropsChildren extends PropsWithChildren<BaseProps> {
|
||||
text?: undefined;
|
||||
}
|
||||
|
||||
interface PropsWithText extends BaseProps {
|
||||
text: JSX.Element;
|
||||
children: never;
|
||||
text: JSX.Element | string;
|
||||
children?: undefined;
|
||||
}
|
||||
|
||||
type Props = PropsWithText | PropsWithChildren;
|
||||
type Props = PropsWithText | PropsChildren;
|
||||
|
||||
export const Button: React.FC<Props> = ({
|
||||
text,
|
||||
type = 'button',
|
||||
onClick,
|
||||
disabled,
|
||||
|
@ -28,6 +28,7 @@ export const Button: React.FC<Props> = ({
|
|||
secondary,
|
||||
className,
|
||||
title,
|
||||
text,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
|
|
|
@ -24,7 +24,7 @@ export type StatusLike = Record<{
|
|||
|
||||
function normalizeHashtag(hashtag: string) {
|
||||
return (
|
||||
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
|
||||
!!hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
|
||||
).normalize('NFKC');
|
||||
}
|
||||
|
||||
|
|
|
@ -191,7 +191,7 @@ const timeRemainingString = (
|
|||
interface Props {
|
||||
intl: IntlShape;
|
||||
timestamp: string;
|
||||
year: number;
|
||||
year?: number;
|
||||
futureDate?: boolean;
|
||||
short?: boolean;
|
||||
}
|
||||
|
@ -203,11 +203,6 @@ class RelativeTimestamp extends Component<Props, States> {
|
|||
now: Date.now(),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
year: new Date().getFullYear(),
|
||||
short: true,
|
||||
};
|
||||
|
||||
_timer: number | undefined;
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: States) {
|
||||
|
@ -257,7 +252,13 @@ class RelativeTimestamp extends Component<Props, States> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { timestamp, intl, year, futureDate, short } = this.props;
|
||||
const {
|
||||
timestamp,
|
||||
intl,
|
||||
futureDate,
|
||||
year = new Date().getFullYear(),
|
||||
short = true,
|
||||
} = this.props;
|
||||
|
||||
const timeGiven = timestamp.includes('T');
|
||||
const date = new Date(timestamp);
|
||||
|
|
|
@ -4,11 +4,10 @@ import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
|||
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||
import type { StatusVisibility } from 'flavours/glitch/models/status';
|
||||
|
||||
import { Icon } from './icon';
|
||||
|
||||
type Visibility = 'public' | 'unlisted' | 'private' | 'direct';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
unlisted_short: {
|
||||
|
@ -25,7 +24,7 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
|
||||
export const VisibilityIcon: React.FC<{ visibility: StatusVisibility }> = ({
|
||||
visibility,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
|
|
@ -3,7 +3,6 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||
import {
|
||||
replyCompose,
|
||||
mentionCompose,
|
||||
|
@ -128,11 +127,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
if (state.getIn(['local_settings', 'confirm_boost_missing_media_description']) && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
|
||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog, missingMediaDescription: true }));
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog, missingMediaDescription: true } }));
|
||||
} else if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -292,7 +291,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
deployPictureInPicture (status, type, mediaProps) {
|
||||
dispatch((_, getState) => {
|
||||
if (getState().getIn(['local_settings', 'media', 'pop_in_player'])) {
|
||||
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
|
||||
dispatch(deployPictureInPicture({statusId: status.get('id'), accountId: status.getIn(['account', 'id']), playerType: type, props: mediaProps}));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -17,8 +17,13 @@ const emojiFilenames = (emojis) => {
|
|||
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺', '📱', '📲', '🚲', '🪮', '🐦⬛']);
|
||||
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️', '🪽', '🪿']);
|
||||
|
||||
const emojiFilename = (filename) => {
|
||||
const borderedEmoji = (document.body && document.body.classList.contains('skin-mastodon-light')) ? lightEmoji : darkEmoji;
|
||||
/**
|
||||
* @param {string} filename
|
||||
* @param {"light" | "dark" } colorScheme
|
||||
* @returns {string}
|
||||
*/
|
||||
const emojiFilename = (filename, colorScheme) => {
|
||||
const borderedEmoji = colorScheme === "light" ? lightEmoji : darkEmoji;
|
||||
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
|
||||
};
|
||||
|
||||
|
@ -92,12 +97,30 @@ const emojifyTextNode = (node, customEmojis) => {
|
|||
const { filename, shortCode } = unicodeMapping[unicode_emoji];
|
||||
const title = shortCode ? `:${shortCode}:` : '';
|
||||
|
||||
replacement = document.createElement('img');
|
||||
replacement.setAttribute('draggable', 'false');
|
||||
replacement.setAttribute('class', 'emojione');
|
||||
replacement.setAttribute('alt', unicode_emoji);
|
||||
replacement.setAttribute('title', title);
|
||||
replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`);
|
||||
replacement = document.createElement('picture');
|
||||
|
||||
const isSystemTheme = !!document.body?.classList.contains('theme-system');
|
||||
|
||||
if(isSystemTheme) {
|
||||
let source = document.createElement('source');
|
||||
source.setAttribute('media', '(prefers-color-scheme: dark)');
|
||||
source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`);
|
||||
replacement.appendChild(source);
|
||||
}
|
||||
|
||||
let img = document.createElement('img');
|
||||
img.setAttribute('draggable', 'false');
|
||||
img.setAttribute('class', 'emojione');
|
||||
img.setAttribute('alt', unicode_emoji);
|
||||
img.setAttribute('title', title);
|
||||
|
||||
let theme = "light";
|
||||
|
||||
if(!isSystemTheme && !document.body?.classList.contains('skin-mastodon-light'))
|
||||
theme = "dark";
|
||||
|
||||
img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`);
|
||||
replacement.appendChild(img);
|
||||
}
|
||||
|
||||
// Add the processed-up-to-now string and the emoji replacement
|
||||
|
|
|
@ -106,7 +106,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
myAccount: ImmutablePropTypes.map,
|
||||
myAccount: ImmutablePropTypes.record,
|
||||
columns: ImmutablePropTypes.list,
|
||||
multiColumn: PropTypes.bool,
|
||||
fetchFollowRequests: PropTypes.func.isRequired,
|
||||
|
|
|
@ -123,7 +123,7 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
|||
<td><FormattedMessage id='keyboard_shortcuts.back' defaultMessage='to navigate back' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>s</kbd></td>
|
||||
<td><kbd>s</kbd>, <kbd>/</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.search' defaultMessage='to focus search' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
@ -27,7 +27,7 @@ export const FilteredNotificationsBanner = () => {
|
|||
};
|
||||
}, [dispatch]);
|
||||
|
||||
if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) * 1 === 0) {
|
||||
if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ export const FilteredNotificationsBanner = () => {
|
|||
|
||||
<div className='filtered-notifications-banner__badge'>
|
||||
<div className='filtered-notifications-banner__badge__badge'>{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}</div>
|
||||
<FormattedMessage id='filtered_notifications_banner.private_mentions' defaultMessage='{count, plural, one {private mention} other {private mentions}}' values={{ count: policy.getIn(['summary', 'pending_notifications_count']) }} />
|
||||
<FormattedMessage id='filtered_notifications_banner.mentions' defaultMessage='{count, plural, one {mention} other {mentions}}' values={{ count: policy.getIn(['summary', 'pending_notifications_count']) }} />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { initBoostModal } from '../../../actions/boosts';
|
||||
import { mentionCompose } from '../../../actions/compose';
|
||||
import {
|
||||
reblog,
|
||||
|
@ -8,6 +7,7 @@ import {
|
|||
unreblog,
|
||||
unfavourite,
|
||||
} from '../../../actions/interactions';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import { boostModal } from '../../../initial_state';
|
||||
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
|
||||
import Notification from '../components/notification';
|
||||
|
@ -46,7 +46,7 @@ const mapDispatchToProps = dispatch => ({
|
|||
if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -14,7 +14,6 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
|||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
||||
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
||||
import { initBoostModal } from 'flavours/glitch/actions/boosts';
|
||||
import { replyCompose } from 'flavours/glitch/actions/compose';
|
||||
import { reblog, favourite, unreblog, unfavourite } from 'flavours/glitch/actions/interactions';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
@ -142,7 +141,7 @@ class Footer extends ImmutablePureComponent {
|
|||
} else if ((e && e.shiftKey) || !boostModal) {
|
||||
this._performReblog(status);
|
||||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this._performReblog }));
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this._performReblog } }));
|
||||
}
|
||||
} else {
|
||||
dispatch(openModal({
|
||||
|
@ -237,4 +236,4 @@ class Footer extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default withRouter(connect(makeMapStateToProps)(injectIntl(Footer)));
|
||||
export default connect(makeMapStateToProps)(withRouter(injectIntl(Footer)));
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: state.getIn(['accounts', accountId]),
|
||||
});
|
||||
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
statusId: PropTypes.string.isRequired,
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, statusId, onClose, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className='picture-in-picture__header'>
|
||||
<Link to={`/@${account.get('acct')}/${statusId}`} className='picture-in-picture__header__account'>
|
||||
<Avatar account={account} size={36} />
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
|
||||
<IconButton icon='times' iconComponent={CloseIcon} onClick={onClose} title={intl.formatMessage(messages.close)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Header));
|
|
@ -0,0 +1,46 @@
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
interface Props {
|
||||
accountId: string;
|
||||
statusId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const Header: React.FC<Props> = ({ accountId, statusId, onClose }) => {
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<div className='picture-in-picture__header'>
|
||||
<Link
|
||||
to={`/@${account.get('acct')}/${statusId}`}
|
||||
className='picture-in-picture__header__account'
|
||||
>
|
||||
<Avatar account={account} size={36} />
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
|
||||
<IconButton
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
title={intl.formatMessage(messages.close)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,93 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { Component } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||
import Audio from 'flavours/glitch/features/audio';
|
||||
import Video from 'flavours/glitch/features/video';
|
||||
|
||||
import Footer from './components/footer';
|
||||
import Header from './components/header';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
...state.get('picture_in_picture'),
|
||||
left: state.getIn(['local_settings', 'media', 'pop_in_position']) === 'left',
|
||||
});
|
||||
|
||||
class PictureInPicture extends Component {
|
||||
|
||||
static propTypes = {
|
||||
statusId: PropTypes.string,
|
||||
accountId: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
src: PropTypes.string,
|
||||
muted: PropTypes.bool,
|
||||
volume: PropTypes.number,
|
||||
currentTime: PropTypes.number,
|
||||
poster: PropTypes.string,
|
||||
backgroundColor: PropTypes.string,
|
||||
foregroundColor: PropTypes.string,
|
||||
accentColor: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
left: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(removePictureInPicture());
|
||||
};
|
||||
|
||||
render () {
|
||||
const { type, src, currentTime, accountId, statusId, left } = this.props;
|
||||
|
||||
if (!currentTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let player;
|
||||
|
||||
if (type === 'video') {
|
||||
player = (
|
||||
<Video
|
||||
src={src}
|
||||
currentTime={this.props.currentTime}
|
||||
volume={this.props.volume}
|
||||
muted={this.props.muted}
|
||||
autoPlay
|
||||
inline
|
||||
alwaysVisible
|
||||
/>
|
||||
);
|
||||
} else if (type === 'audio') {
|
||||
player = (
|
||||
<Audio
|
||||
src={src}
|
||||
currentTime={this.props.currentTime}
|
||||
volume={this.props.volume}
|
||||
muted={this.props.muted}
|
||||
poster={this.props.poster}
|
||||
backgroundColor={this.props.backgroundColor}
|
||||
foregroundColor={this.props.foregroundColor}
|
||||
accentColor={this.props.accentColor}
|
||||
autoPlay
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('picture-in-picture', { left })}>
|
||||
<Header accountId={accountId} statusId={statusId} onClose={this.handleClose} />
|
||||
|
||||
{player}
|
||||
|
||||
<Footer statusId={statusId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(PictureInPicture);
|
|
@ -0,0 +1,90 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||
import Audio from 'flavours/glitch/features/audio';
|
||||
import Video from 'flavours/glitch/features/video';
|
||||
import {
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from 'flavours/glitch/store/typed_functions';
|
||||
|
||||
import Footer from './components/footer';
|
||||
import { Header } from './components/header';
|
||||
|
||||
export const PictureInPicture: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(removePictureInPicture());
|
||||
}, [dispatch]);
|
||||
|
||||
const pipState = useAppSelector((s) => s.picture_in_picture);
|
||||
|
||||
const left = useAppSelector(
|
||||
// @ts-expect-error - `local_settings` is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
(s) => s.getIn(['local_settings', 'media', 'pop_in_position']) === 'left',
|
||||
);
|
||||
|
||||
if (pipState.type === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
type,
|
||||
src,
|
||||
currentTime,
|
||||
accountId,
|
||||
statusId,
|
||||
volume,
|
||||
muted,
|
||||
poster,
|
||||
backgroundColor,
|
||||
foregroundColor,
|
||||
accentColor,
|
||||
} = pipState;
|
||||
|
||||
let player;
|
||||
|
||||
switch (type) {
|
||||
case 'video':
|
||||
player = (
|
||||
<Video
|
||||
src={src}
|
||||
currentTime={currentTime}
|
||||
volume={volume}
|
||||
muted={muted}
|
||||
autoPlay
|
||||
inline
|
||||
alwaysVisible
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'audio':
|
||||
player = (
|
||||
<Audio
|
||||
src={src}
|
||||
currentTime={currentTime}
|
||||
volume={volume}
|
||||
muted={muted}
|
||||
poster={poster}
|
||||
backgroundColor={backgroundColor}
|
||||
foregroundColor={foregroundColor}
|
||||
accentColor={accentColor}
|
||||
autoPlay
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('picture-in-picture', { left })}>
|
||||
<Header accountId={accountId} statusId={statusId} onClose={handleClose} />
|
||||
|
||||
{player}
|
||||
|
||||
<Footer statusId={statusId} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -4,7 +4,6 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { showAlertForError } from '../../../actions/alerts';
|
||||
import { initBlockModal } from '../../../actions/blocks';
|
||||
import { initBoostModal } from '../../../actions/boosts';
|
||||
import {
|
||||
replyCompose,
|
||||
mentionCompose,
|
||||
|
@ -82,7 +81,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this.onModalReblog }));
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -25,7 +25,6 @@ import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning';
|
|||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import { initBlockModal } from '../../actions/blocks';
|
||||
import { initBoostModal } from '../../actions/boosts';
|
||||
import {
|
||||
replyCompose,
|
||||
mentionCompose,
|
||||
|
@ -377,11 +376,11 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
if (signedIn) {
|
||||
if (settings.get('confirm_boost_missing_media_description') && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
|
||||
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog, missingMediaDescription: true }));
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.handleModalReblog, missingMediaDescription: true } }));
|
||||
} else if ((e && e.shiftKey) || !boostModal) {
|
||||
this.handleModalReblog(status);
|
||||
} else {
|
||||
dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.handleModalReblog } }));
|
||||
}
|
||||
} else {
|
||||
dispatch(openModal({
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import { changeBoostPrivacy } from 'flavours/glitch/actions/boosts';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
|
||||
import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { Button } from '../../../components/button';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { RelativeTimestamp } from '../../../components/relative_timestamp';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
privacy: state.getIn(['boosts', 'new', 'privacy']),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChangeBoostPrivacy(value) {
|
||||
dispatch(changeBoostPrivacy(value));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
class BoostModal extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
onReblog: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
missingMediaDescription: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
handleReblog = () => {
|
||||
this.props.onReblog(this.props.status, this.props.privacy);
|
||||
this.props.onClose();
|
||||
};
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this.props.onClose();
|
||||
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||
}
|
||||
};
|
||||
|
||||
_findContainer = () => {
|
||||
return document.getElementsByClassName('modal-root__container')[0];
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, missingMediaDescription, privacy, intl } = this.props;
|
||||
const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal boost-modal'>
|
||||
<div className='boost-modal__container'>
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, 'light')}>
|
||||
<div className='status__info'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility')} /></span>
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
||||
</a>
|
||||
|
||||
<a onClick={this.handleAccountClick} href={status.getIn(['account', 'url'])} className='status__display-name'>
|
||||
<div className='status__avatar'>
|
||||
<Avatar account={status.get('account')} size={48} />
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<StatusContent status={status} />
|
||||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='boost-modal__action-bar'>
|
||||
<div>
|
||||
{ missingMediaDescription ?
|
||||
<FormattedMessage id='boost_modal.missing_description' defaultMessage='This toot contains some media without description' />
|
||||
:
|
||||
<FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' icon={RepeatIcon} /></span> }} />
|
||||
}
|
||||
</div>
|
||||
|
||||
{status.get('visibility') !== 'private' && !status.get('reblogged') && (
|
||||
<PrivacyDropdown
|
||||
noDirect
|
||||
value={privacy}
|
||||
container={this._findContainer}
|
||||
onChange={this.props.onChangeBoostPrivacy}
|
||||
/>
|
||||
)}
|
||||
<Button text={intl.formatMessage(buttonText)} onClick={this.handleReblog} autoFocus />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal)));
|
|
@ -0,0 +1,170 @@
|
|||
import type { MouseEventHandler } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import type Immutable from 'immutable';
|
||||
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
|
||||
import PrivacyDropdown from 'flavours/glitch/features/compose/components/privacy_dropdown';
|
||||
import type { Account } from 'flavours/glitch/models/account';
|
||||
import type { Status, StatusVisibility } from 'flavours/glitch/models/status';
|
||||
import { useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { Button } from '../../../components/button';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { RelativeTimestamp } from '../../../components/relative_timestamp';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel_reblog: {
|
||||
id: 'status.cancel_reblog_private',
|
||||
defaultMessage: 'Unboost',
|
||||
},
|
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||
});
|
||||
|
||||
export const BoostModal: React.FC<{
|
||||
status: Status;
|
||||
onClose: () => void;
|
||||
onReblog: (status: Status, privacy: StatusVisibility) => void;
|
||||
missingMediaDescription?: boolean;
|
||||
}> = ({ status, onReblog, onClose, missingMediaDescription }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const default_privacy = useAppSelector(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
(state) => state.compose.get('default_privacy') as StatusVisibility,
|
||||
);
|
||||
|
||||
const account = status.get('account') as Account;
|
||||
const statusVisibility = status.get('visibility') as StatusVisibility;
|
||||
|
||||
const [privacy, setPrivacy] = useState<StatusVisibility>(
|
||||
statusVisibility === 'private' ? 'private' : default_privacy,
|
||||
);
|
||||
|
||||
const onPrivacyChange = useCallback((value: StatusVisibility) => {
|
||||
setPrivacy(value);
|
||||
}, []);
|
||||
|
||||
const handleReblog = useCallback(() => {
|
||||
onReblog(status, privacy);
|
||||
onClose();
|
||||
}, [onClose, onReblog, status, privacy]);
|
||||
|
||||
const handleAccountClick = useCallback<MouseEventHandler>(
|
||||
(e) => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
history.push(`/@${account.acct}`);
|
||||
}
|
||||
},
|
||||
[history, onClose, account],
|
||||
);
|
||||
|
||||
const buttonText = status.get('reblogged')
|
||||
? messages.cancel_reblog
|
||||
: messages.reblog;
|
||||
|
||||
const findContainer = useCallback(
|
||||
() => document.getElementsByClassName('modal-root__container')[0],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal boost-modal'>
|
||||
<div className='boost-modal__container'>
|
||||
<div
|
||||
className={classNames(
|
||||
'status',
|
||||
`status-${statusVisibility}`,
|
||||
'light',
|
||||
)}
|
||||
>
|
||||
<div className='status__info'>
|
||||
<a
|
||||
href={status.get('url') as string}
|
||||
className='status__relative-time'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<span className='status__visibility-icon'>
|
||||
<VisibilityIcon visibility={statusVisibility} />
|
||||
</span>
|
||||
<RelativeTimestamp
|
||||
timestamp={status.get('created_at') as string}
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
onClick={handleAccountClick}
|
||||
href={account.url}
|
||||
className='status__display-name'
|
||||
>
|
||||
<div className='status__avatar'>
|
||||
<Avatar account={account} size={48} />
|
||||
</div>
|
||||
|
||||
<DisplayName account={account} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* @ts-expect-error Expected until StatusContent is typed */}
|
||||
<StatusContent status={status} />
|
||||
|
||||
{(status.get('media_attachments') as Immutable.List<unknown>).size >
|
||||
0 && (
|
||||
<AttachmentList compact media={status.get('media_attachments')} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='boost-modal__action-bar'>
|
||||
<div>
|
||||
{missingMediaDescription ? (
|
||||
<FormattedMessage
|
||||
id='boost_modal.missing_description'
|
||||
defaultMessage='This toot contains some media without description'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='boost_modal.combo'
|
||||
defaultMessage='You can press {combo} to skip this next time'
|
||||
values={{
|
||||
combo: (
|
||||
<span>
|
||||
Shift + <Icon id='retweet' icon={RepeatIcon} />
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{statusVisibility !== 'private' && !status.get('reblogged') && (
|
||||
<PrivacyDropdown
|
||||
noDirect
|
||||
value={privacy}
|
||||
container={findContainer}
|
||||
onChange={onPrivacyChange}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
text={intl.formatMessage(buttonText)}
|
||||
onClick={handleReblog}
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -26,7 +26,7 @@ import BundleContainer from '../containers/bundle_container';
|
|||
|
||||
import ActionsModal from './actions_modal';
|
||||
import AudioModal from './audio_modal';
|
||||
import BoostModal from './boost_modal';
|
||||
import { BoostModal } from './boost_modal';
|
||||
import BundleModalError from './bundle_modal_error';
|
||||
import ConfirmationModal from './confirmation_modal';
|
||||
import DeprecatedSettingsModal from './deprecated_settings_modal';
|
||||
|
|
|
@ -16,7 +16,7 @@ import { changeLayout } from 'flavours/glitch/actions/app';
|
|||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
||||
import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import PictureInPicture from 'flavours/glitch/features/picture_in_picture';
|
||||
import { PictureInPicture } from 'flavours/glitch/features/picture_in_picture';
|
||||
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
|
@ -96,7 +96,7 @@ const mapStateToProps = state => ({
|
|||
const keyMap = {
|
||||
help: '?',
|
||||
new: 'n',
|
||||
search: 's',
|
||||
search: ['s', '/'],
|
||||
forceNew: 'option+n',
|
||||
toggleComposeSpoilers: 'option+x',
|
||||
focusColumn: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
|
|
4
app/javascript/flavours/glitch/models/status.ts
Normal file
4
app/javascript/flavours/glitch/models/status.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export type { StatusVisibility } from 'flavours/glitch/api_types/statuses';
|
||||
|
||||
// Temporary until we type it correctly
|
||||
export type Status = Immutable.Map<string, unknown>;
|
|
@ -1,8 +1,265 @@
|
|||
import 'packs/public-path';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import Rails from '@rails/ujs';
|
||||
|
||||
import ready from 'flavours/glitch/ready';
|
||||
|
||||
const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
|
||||
const valid = target.value && target.validity.valid;
|
||||
const element = document.querySelector<HTMLInputElement>(
|
||||
'input[type="datetime-local"]#announcement_ends_at',
|
||||
);
|
||||
|
||||
if (!element) return;
|
||||
|
||||
if (valid) {
|
||||
element.classList.remove('optional');
|
||||
element.required = true;
|
||||
element.min = target.value;
|
||||
} else {
|
||||
element.classList.add('optional');
|
||||
element.removeAttribute('required');
|
||||
element.removeAttribute('min');
|
||||
}
|
||||
};
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'input[type="datetime-local"]#announcement_starts_at',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLInputElement)
|
||||
setAnnouncementEndsAttributes(target);
|
||||
},
|
||||
);
|
||||
|
||||
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
|
||||
|
||||
const showSelectAll = () => {
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
selectAllMatchingElement?.classList.add('active');
|
||||
};
|
||||
|
||||
const hideSelectAll = () => {
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
const hiddenField = document.querySelector<HTMLInputElement>(
|
||||
'input#select_all_matching',
|
||||
);
|
||||
const selectedMsg = document.querySelector(
|
||||
'.batch-table__select-all .selected',
|
||||
);
|
||||
const notSelectedMsg = document.querySelector(
|
||||
'.batch-table__select-all .not-selected',
|
||||
);
|
||||
|
||||
selectAllMatchingElement?.classList.remove('active');
|
||||
selectedMsg?.classList.remove('active');
|
||||
notSelectedMsg?.classList.add('active');
|
||||
if (hiddenField) hiddenField.value = '0';
|
||||
};
|
||||
|
||||
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>(batchCheckboxClassName)
|
||||
.forEach((content) => {
|
||||
content.checked = target.checked;
|
||||
});
|
||||
|
||||
if (selectAllMatchingElement) {
|
||||
if (target.checked) {
|
||||
showSelectAll();
|
||||
} else {
|
||||
hideSelectAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
|
||||
const hiddenField = document.querySelector<HTMLInputElement>(
|
||||
'#select_all_matching',
|
||||
);
|
||||
|
||||
if (!hiddenField) return;
|
||||
|
||||
const active = hiddenField.value === '1';
|
||||
const selectedMsg = document.querySelector(
|
||||
'.batch-table__select-all .selected',
|
||||
);
|
||||
const notSelectedMsg = document.querySelector(
|
||||
'.batch-table__select-all .not-selected',
|
||||
);
|
||||
|
||||
if (!selectedMsg || !notSelectedMsg) return;
|
||||
|
||||
if (active) {
|
||||
hiddenField.value = '0';
|
||||
selectedMsg.classList.remove('active');
|
||||
notSelectedMsg.classList.add('active');
|
||||
} else {
|
||||
hiddenField.value = '1';
|
||||
notSelectedMsg.classList.remove('active');
|
||||
selectedMsg.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
|
||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
const selectAllMatchingElement = document.querySelector(
|
||||
'.batch-table__select-all',
|
||||
);
|
||||
|
||||
if (checkAllElement) {
|
||||
const allCheckboxes = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
|
||||
);
|
||||
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
|
||||
checkAllElement.indeterminate =
|
||||
!checkAllElement.checked &&
|
||||
allCheckboxes.some((content) => content.checked);
|
||||
|
||||
if (selectAllMatchingElement) {
|
||||
if (checkAllElement.checked) {
|
||||
showSelectAll();
|
||||
} else {
|
||||
hideSelectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'.filter-subset--with-select select',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) target.form?.submit();
|
||||
},
|
||||
);
|
||||
|
||||
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
|
||||
const rejectMediaDiv = document.querySelector(
|
||||
'.input.with_label.domain_block_reject_media',
|
||||
);
|
||||
const rejectReportsDiv = document.querySelector(
|
||||
'.input.with_label.domain_block_reject_reports',
|
||||
);
|
||||
|
||||
if (rejectMediaDiv && rejectMediaDiv instanceof HTMLElement) {
|
||||
rejectMediaDiv.style.display =
|
||||
target.value === 'suspend' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
if (rejectReportsDiv && rejectReportsDiv instanceof HTMLElement) {
|
||||
rejectReportsDiv.style.display =
|
||||
target.value === 'suspend' ? 'none' : 'block';
|
||||
}
|
||||
};
|
||||
|
||||
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
|
||||
});
|
||||
|
||||
const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
|
||||
const bootstrapTimelineAccountsField =
|
||||
document.querySelector<HTMLInputElement>(
|
||||
'#form_admin_settings_bootstrap_timeline_accounts',
|
||||
);
|
||||
|
||||
if (bootstrapTimelineAccountsField) {
|
||||
bootstrapTimelineAccountsField.disabled = !target.checked;
|
||||
if (target.checked) {
|
||||
bootstrapTimelineAccountsField.parentElement?.classList.remove(
|
||||
'disabled',
|
||||
);
|
||||
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.remove(
|
||||
'disabled',
|
||||
);
|
||||
} else {
|
||||
bootstrapTimelineAccountsField.parentElement?.classList.add('disabled');
|
||||
bootstrapTimelineAccountsField.parentElement?.parentElement?.classList.add(
|
||||
'disabled',
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#form_admin_settings_enable_bootstrap_timeline_accounts',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLInputElement)
|
||||
onEnableBootstrapTimelineAccountsChange(target);
|
||||
},
|
||||
);
|
||||
|
||||
const onChangeRegistrationMode = (target: HTMLSelectElement) => {
|
||||
const enabled = target.value === 'approved';
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLElement>(
|
||||
'.form_admin_settings_registrations_mode .warning-hint',
|
||||
)
|
||||
.forEach((warning_hint) => {
|
||||
warning_hint.style.display = target.value === 'open' ? 'inline' : 'none';
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>(
|
||||
'input#form_admin_settings_require_invite_text',
|
||||
)
|
||||
.forEach((input) => {
|
||||
input.disabled = !enabled;
|
||||
if (enabled) {
|
||||
let element: HTMLElement | null = input;
|
||||
do {
|
||||
element.classList.remove('disabled');
|
||||
element = element.parentElement;
|
||||
} while (element && !element.classList.contains('fields-group'));
|
||||
} else {
|
||||
let element: HTMLElement | null = input;
|
||||
do {
|
||||
element.classList.add('disabled');
|
||||
element = element.parentElement;
|
||||
} while (element && !element.classList.contains('fields-group'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const convertUTCDateTimeToLocal = (value: string) => {
|
||||
const date = new Date(value + 'Z');
|
||||
const twoChars = (x: number) => x.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
function convertLocalDatetimeToUTC(value: string) {
|
||||
const date = new Date(value);
|
||||
const fullISO8601 = date.toISOString();
|
||||
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
||||
}
|
||||
|
||||
Rails.delegate(
|
||||
document,
|
||||
'#form_admin_settings_registrations_mode',
|
||||
'change',
|
||||
({ target }) => {
|
||||
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
|
||||
},
|
||||
);
|
||||
|
||||
async function mountReactComponent(element: Element) {
|
||||
const componentName = element.getAttribute('data-admin-component');
|
||||
const stringProps = element.getAttribute('data-props');
|
||||
|
@ -29,9 +286,83 @@ async function mountReactComponent(element: Element) {
|
|||
}
|
||||
|
||||
ready(() => {
|
||||
const domainBlockSeveritySelect = document.querySelector<HTMLSelectElement>(
|
||||
'select#domain_block_severity',
|
||||
);
|
||||
if (domainBlockSeveritySelect)
|
||||
onDomainBlockSeverityChange(domainBlockSeveritySelect);
|
||||
|
||||
const enableBootstrapTimelineAccounts =
|
||||
document.querySelector<HTMLInputElement>(
|
||||
'input#form_admin_settings_enable_bootstrap_timeline_accounts',
|
||||
);
|
||||
if (enableBootstrapTimelineAccounts)
|
||||
onEnableBootstrapTimelineAccountsChange(enableBootstrapTimelineAccounts);
|
||||
|
||||
const registrationMode = document.querySelector<HTMLSelectElement>(
|
||||
'select#form_admin_settings_registrations_mode',
|
||||
);
|
||||
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
||||
|
||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||
'input#batch_checkbox_all',
|
||||
);
|
||||
if (checkAllElement) {
|
||||
const allCheckboxes = Array.from(
|
||||
document.querySelectorAll<HTMLInputElement>(batchCheckboxClassName),
|
||||
);
|
||||
checkAllElement.checked = allCheckboxes.every((content) => content.checked);
|
||||
checkAllElement.indeterminate =
|
||||
!checkAllElement.checked &&
|
||||
allCheckboxes.some((content) => content.checked);
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector('a#add-instance-button')
|
||||
?.addEventListener('click', (e) => {
|
||||
const domain = document.querySelector<HTMLInputElement>(
|
||||
'input[type="text"]#by_domain',
|
||||
)?.value;
|
||||
|
||||
if (domain && e.target instanceof HTMLAnchorElement) {
|
||||
const url = new URL(e.target.href);
|
||||
url.searchParams.set('_domain', domain);
|
||||
e.target.href = url.toString();
|
||||
}
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
||||
.forEach((element) => {
|
||||
if (element.value) {
|
||||
element.value = convertUTCDateTimeToLocal(element.value);
|
||||
}
|
||||
if (element.placeholder) {
|
||||
element.placeholder = convertUTCDateTimeToLocal(element.placeholder);
|
||||
}
|
||||
});
|
||||
|
||||
Rails.delegate(document, 'form', 'submit', ({ target }) => {
|
||||
if (target instanceof HTMLFormElement)
|
||||
target
|
||||
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
||||
.forEach((element) => {
|
||||
if (element.value && element.validity.valid) {
|
||||
element.value = convertLocalDatetimeToUTC(element.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const announcementStartsAt = document.querySelector<HTMLInputElement>(
|
||||
'input[type="datetime-local"]#announcement_starts_at',
|
||||
);
|
||||
if (announcementStartsAt) {
|
||||
setAnnouncementEndsAttributes(announcementStartsAt);
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-admin-component]').forEach((element) => {
|
||||
void mountReactComponent(element);
|
||||
});
|
||||
}).catch((reason) => {
|
||||
}).catch((reason: unknown) => {
|
||||
throw reason;
|
||||
});
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import 'packs/public-path';
|
||||
|
||||
import { start } from 'flavours/glitch/common';
|
||||
import { loadLocale } from 'flavours/glitch/locales';
|
||||
import main from "flavours/glitch/main";
|
||||
import { loadPolyfills } from 'flavours/glitch/polyfills';
|
||||
|
||||
start();
|
||||
|
||||
loadPolyfills()
|
||||
.then(loadLocale)
|
||||
.then(main)
|
|
@ -1,8 +1,8 @@
|
|||
/* This file is a hack to have something more reliable than the upstream `common` tag
|
||||
that is implicitly generated as the common chunk through webpack's `splitChunks` config */
|
||||
|
||||
import 'packs/public-path';
|
||||
import Rails from '@rails/ujs';
|
||||
import 'flavours/glitch/styles/index.scss';
|
||||
import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
Rails.start();
|
||||
|
||||
// This ensures that webpack compiles our images.
|
||||
// This is a hack to ensures that webpack compiles our images.
|
||||
require.context('../images', true);
|
||||
|
|
4
app/javascript/flavours/glitch/packs/inert.js
Normal file
4
app/javascript/flavours/glitch/packs/inert.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
/* Placeholder file to have `inert.scss` compiled by Webpack
|
||||
This is used by the `wicg-inert` polyfill */
|
||||
|
||||
import '@/styles/inert.scss';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue