mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2024-11-22 07:08:07 +01:00
Merge branch 'refs/heads/glitch-soc' into develop
This commit is contained in:
commit
e73c612cb1
735 changed files with 12737 additions and 6088 deletions
|
@ -70,7 +70,7 @@ services:
|
||||||
hard: -1
|
hard: -1
|
||||||
|
|
||||||
libretranslate:
|
libretranslate:
|
||||||
image: libretranslate/libretranslate:v1.5.6
|
image: libretranslate/libretranslate:v1.5.7
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- lt-data:/home/libretranslate/.local
|
- lt-data:/home/libretranslate/.local
|
||||||
|
|
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
|
# Federation
|
||||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
LOCAL_HTTPS=true
|
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."
|
"message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"@typescript-eslint/restrict-template-expressions": ['warn', { allowNumber: true }],
|
||||||
'jsdoc/require-jsdoc': 'off',
|
'jsdoc/require-jsdoc': 'off',
|
||||||
|
|
||||||
// Those rules set stricter rules for TS files
|
// Those rules set stricter rules for TS files
|
||||||
|
|
21
.github/stylelint-matcher.json
vendored
21
.github/stylelint-matcher.json
vendored
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"problemMatcher": [
|
|
||||||
{
|
|
||||||
"owner": "stylelint",
|
|
||||||
"pattern": [
|
|
||||||
{
|
|
||||||
"regexp": "^([^\\s].*)$",
|
|
||||||
"file": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"regexp": "^\\s+((\\d+):(\\d+))?\\s+(✖|×)\\s+(.*)\\s{2,}(.*)$",
|
|
||||||
"line": 2,
|
|
||||||
"column": 3,
|
|
||||||
"message": 5,
|
|
||||||
"code": 6,
|
|
||||||
"loop": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
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
|
# Create or update the pull request
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v6.0.2
|
uses: peter-evans/create-pull-request@v6.0.5
|
||||||
with:
|
with:
|
||||||
commit-message: 'New Crowdin translations'
|
commit-message: 'New Crowdin translations'
|
||||||
title: 'New Crowdin Translations (automated)'
|
title: 'New Crowdin Translations (automated)'
|
||||||
|
|
6
.github/workflows/lint-css.yml
vendored
6
.github/workflows/lint-css.yml
vendored
|
@ -38,9 +38,5 @@ jobs:
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|
||||||
- uses: xt0rted/stylelint-problem-matcher@v1
|
|
||||||
|
|
||||||
- run: echo "::add-matcher::.github/stylelint-matcher.json"
|
|
||||||
|
|
||||||
- name: Stylelint
|
- name: Stylelint
|
||||||
run: yarn lint:css
|
run: yarn lint:css -f github
|
||||||
|
|
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
|
@ -38,5 +38,5 @@ jobs:
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|
||||||
- name: Jest testing
|
- name: JavaScript testing
|
||||||
run: yarn jest --reporters github-actions summary
|
run: yarn jest --reporters github-actions summary
|
||||||
|
|
12
.github/workflows/test-ruby.yml
vendored
12
.github/workflows/test-ruby.yml
vendored
|
@ -28,6 +28,9 @@ jobs:
|
||||||
env:
|
env:
|
||||||
RAILS_ENV: ${{ matrix.mode }}
|
RAILS_ENV: ${{ matrix.mode }}
|
||||||
BUNDLE_WITH: ${{ matrix.mode }}
|
BUNDLE_WITH: ${{ matrix.mode }}
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: precompile_placeholder
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: precompile_placeholder
|
||||||
|
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: precompile_placeholder
|
||||||
OTP_SECRET: precompile_placeholder
|
OTP_SECRET: precompile_placeholder
|
||||||
SECRET_KEY_BASE: precompile_placeholder
|
SECRET_KEY_BASE: precompile_placeholder
|
||||||
|
|
||||||
|
@ -111,10 +114,9 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- '3.0'
|
|
||||||
- '3.1'
|
- '3.1'
|
||||||
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
- '3.3'
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
@ -187,10 +189,9 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- '3.0'
|
|
||||||
- '3.1'
|
- '3.1'
|
||||||
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
- '3.3'
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -287,10 +288,9 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
ruby-version:
|
ruby-version:
|
||||||
- '3.0'
|
|
||||||
- '3.1'
|
- '3.1'
|
||||||
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
- '3.3'
|
|
||||||
search-image:
|
search-image:
|
||||||
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
||||||
include:
|
include:
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -24,7 +24,6 @@
|
||||||
/public/packs-test
|
/public/packs-test
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
.env.development
|
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
|
@ -69,3 +68,6 @@ yarn-debug.log
|
||||||
|
|
||||||
# Ignore Docker option files
|
# Ignore Docker option files
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# Ignore dotenv .local files
|
||||||
|
.env*.local
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
exclude:
|
exclude:
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
- lib/templates/haml/scaffold/_form.html.haml
|
|
||||||
|
|
||||||
require:
|
require:
|
||||||
- ./lib/linter/haml_middle_dot.rb
|
- ./lib/linter/haml_middle_dot.rb
|
||||||
|
@ -11,6 +10,6 @@ linters:
|
||||||
MiddleDot:
|
MiddleDot:
|
||||||
enabled: true
|
enabled: true
|
||||||
LineLength:
|
LineLength:
|
||||||
max: 320
|
max: 300
|
||||||
ViewLength:
|
ViewLength:
|
||||||
max: 200 # Override default value of 100 inherited from rubocop
|
max: 200 # Override default value of 100 inherited from rubocop
|
||||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
20.11
|
20.12
|
||||||
|
|
32
.rubocop.yml
32
.rubocop.yml
|
@ -9,12 +9,13 @@ inherit_mode:
|
||||||
require:
|
require:
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
- rubocop-rspec
|
- rubocop-rspec
|
||||||
|
- rubocop-rspec_rails
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-capybara
|
- rubocop-capybara
|
||||||
- ./lib/linter/rubocop_middle_dot
|
- ./lib/linter/rubocop_middle_dot
|
||||||
|
|
||||||
AllCops:
|
AllCops:
|
||||||
TargetRubyVersion: 3.0 # Set to minimum supported version of CI
|
TargetRubyVersion: 3.1 # Set to minimum supported version of CI
|
||||||
DisplayCopNames: true
|
DisplayCopNames: true
|
||||||
DisplayStyleGuide: true
|
DisplayStyleGuide: true
|
||||||
ExtraDetails: true
|
ExtraDetails: true
|
||||||
|
@ -39,13 +40,7 @@ Layout/FirstHashElementIndentation:
|
||||||
# Reason: Currently disabled in .rubocop_todo.yml
|
# Reason: Currently disabled in .rubocop_todo.yml
|
||||||
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
|
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Max: 320 # Default of 120 causes a duplicate entry in generated todo file
|
Max: 300 # 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
|
|
||||||
|
|
||||||
## Disable most Metrics/*Length cops
|
## Disable most Metrics/*Length cops
|
||||||
# Reason: those are often triggered and force significant refactors when this happend
|
# Reason: those are often triggered and force significant refactors when this happend
|
||||||
|
@ -86,6 +81,11 @@ Metrics/CyclomaticComplexity:
|
||||||
Metrics/ParameterLists:
|
Metrics/ParameterLists:
|
||||||
CountKeywordArgs: false
|
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
|
# Reason: Prevailing style is argument file paths
|
||||||
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
|
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
|
||||||
Rails/FilePath:
|
Rails/FilePath:
|
||||||
|
@ -148,11 +148,6 @@ RSpec/NamedSubject:
|
||||||
RSpec/NotToNot:
|
RSpec/NotToNot:
|
||||||
EnforcedStyle: to_not
|
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
|
# Reason: Match overrides from Rspec/FilePath rule above
|
||||||
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat
|
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat
|
||||||
RSpec/SpecFilePathFormat:
|
RSpec/SpecFilePathFormat:
|
||||||
|
@ -163,6 +158,11 @@ RSpec/SpecFilePathFormat:
|
||||||
OEmbedController: oembed_controller
|
OEmbedController: oembed_controller
|
||||||
OStatus: ostatus
|
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:
|
# Reason:
|
||||||
# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren
|
# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren
|
||||||
Style/ClassAndModuleChildren:
|
Style/ClassAndModuleChildren:
|
||||||
|
@ -182,10 +182,16 @@ Style/FormatStringToken:
|
||||||
AllowedMethods:
|
AllowedMethods:
|
||||||
- redirect_with_vary
|
- redirect_with_vary
|
||||||
|
|
||||||
|
# Reason: Prevailing style choice
|
||||||
|
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashaslastarrayitem
|
||||||
|
Style/HashAsLastArrayItem:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
# Reason: Enforce modern Ruby style
|
# Reason: Enforce modern Ruby style
|
||||||
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax
|
# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax
|
||||||
Style/HashSyntax:
|
Style/HashSyntax:
|
||||||
EnforcedStyle: ruby19_no_mixed_keys
|
EnforcedStyle: ruby19_no_mixed_keys
|
||||||
|
EnforcedShorthandSyntax: either
|
||||||
|
|
||||||
# Reason:
|
# Reason:
|
||||||
# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals
|
# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals
|
||||||
|
|
|
@ -1,18 +1,11 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
# `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
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
# versions of RuboCop, may require this file to be generated again.
|
# 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:
|
Lint/NonLocalExitFromIterator:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/helpers/jsonld_helper.rb'
|
- 'app/helpers/jsonld_helper.rb'
|
||||||
|
@ -36,7 +29,7 @@ Metrics/PerceivedComplexity:
|
||||||
|
|
||||||
# Configuration parameters: CountAsOne.
|
# Configuration parameters: CountAsOne.
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 20 # Override default of 5
|
Max: 18
|
||||||
|
|
||||||
RSpec/MultipleExpectations:
|
RSpec/MultipleExpectations:
|
||||||
Max: 7
|
Max: 7
|
||||||
|
@ -49,27 +42,10 @@ RSpec/MultipleMemoizedHelpers:
|
||||||
RSpec/NestedGroups:
|
RSpec/NestedGroups:
|
||||||
Max: 6
|
Max: 6
|
||||||
|
|
||||||
# Configuration parameters: Include.
|
|
||||||
# Include: app/models/**/*.rb
|
|
||||||
Rails/HasAndBelongsToMany:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/concerns/account/associations.rb'
|
|
||||||
- 'app/models/status.rb'
|
|
||||||
- 'app/models/tag.rb'
|
|
||||||
|
|
||||||
Rails/OutputSafety:
|
Rails/OutputSafety:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/simple_form.rb'
|
- '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).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||||
# AllowedMethods: ==, equal?, eql?
|
# AllowedMethods: ==, equal?, eql?
|
||||||
|
@ -88,7 +64,6 @@ Style/FetchEnvVar:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/lib/redis_configuration.rb'
|
- 'app/lib/redis_configuration.rb'
|
||||||
- 'app/lib/translation_service.rb'
|
- 'app/lib/translation_service.rb'
|
||||||
- 'config/environments/development.rb'
|
|
||||||
- 'config/environments/production.rb'
|
- 'config/environments/production.rb'
|
||||||
- 'config/initializers/2_limited_federation_mode.rb'
|
- 'config/initializers/2_limited_federation_mode.rb'
|
||||||
- 'config/initializers/3_omniauth.rb'
|
- 'config/initializers/3_omniauth.rb'
|
||||||
|
@ -98,7 +73,6 @@ Style/FetchEnvVar:
|
||||||
- 'config/initializers/paperclip.rb'
|
- 'config/initializers/paperclip.rb'
|
||||||
- 'config/initializers/vapid.rb'
|
- 'config/initializers/vapid.rb'
|
||||||
- 'lib/mastodon/redis_config.rb'
|
- 'lib/mastodon/redis_config.rb'
|
||||||
- 'lib/premailer_webpack_strategy.rb'
|
|
||||||
- 'lib/tasks/repo.rake'
|
- 'lib/tasks/repo.rake'
|
||||||
- 'spec/features/profile_spec.rb'
|
- 'spec/features/profile_spec.rb'
|
||||||
|
|
||||||
|
@ -144,22 +118,8 @@ Style/GuardClause:
|
||||||
- 'lib/mastodon/cli/accounts.rb'
|
- 'lib/mastodon/cli/accounts.rb'
|
||||||
- 'lib/mastodon/cli/maintenance.rb'
|
- 'lib/mastodon/cli/maintenance.rb'
|
||||||
- 'lib/mastodon/cli/media.rb'
|
- 'lib/mastodon/cli/media.rb'
|
||||||
- 'lib/paperclip/attachment_extensions.rb'
|
|
||||||
- 'lib/tasks/repo.rake'
|
- 'lib/tasks/repo.rake'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: braces, no_braces
|
|
||||||
Style/HashAsLastArrayItem:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/admin/statuses_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/statuses_controller.rb'
|
|
||||||
- 'app/models/concerns/account/counters.rb'
|
|
||||||
- 'app/models/concerns/status/threading_concern.rb'
|
|
||||||
- 'app/models/status.rb'
|
|
||||||
- 'app/services/batched_remove_status_service.rb'
|
|
||||||
- 'app/services/notify_service.rb'
|
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
Style/HashTransformValues:
|
Style/HashTransformValues:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -207,13 +167,6 @@ Style/OptionalBooleanParameter:
|
||||||
- 'app/workers/unfollow_follow_worker.rb'
|
- 'app/workers/unfollow_follow_worker.rb'
|
||||||
- 'lib/mastodon/redis_config.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).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: EnforcedStyle.
|
# Configuration parameters: EnforcedStyle.
|
||||||
# SupportedStyles: short, verbose
|
# SupportedStyles: short, verbose
|
||||||
|
@ -244,52 +197,12 @@ Style/SafeNavigation:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/concerns/account/finder_concern.rb'
|
- 'app/models/concerns/account/finder_concern.rb'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: only_raise, only_fail, semantic
|
|
||||||
Style/SignalException:
|
|
||||||
Exclude:
|
|
||||||
- '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).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: Mode.
|
# Configuration parameters: Mode.
|
||||||
Style/StringConcatenation:
|
Style/StringConcatenation:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'config/initializers/paperclip.rb'
|
- '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).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: WordRegex.
|
# Configuration parameters: WordRegex.
|
||||||
# SupportedStyles: percent, brackets
|
# SupportedStyles: percent, brackets
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
3.3.0
|
3.3.1
|
||||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -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
|
# Please see https://docs.docker.com/engine/reference/builder for information about
|
||||||
# the extended buildx capabilities used in this file.
|
# the extended buildx capabilities used in this file.
|
||||||
|
@ -7,15 +7,15 @@
|
||||||
ARG TARGETPLATFORM=${TARGETPLATFORM}
|
ARG TARGETPLATFORM=${TARGETPLATFORM}
|
||||||
ARG BUILDPLATFORM=${BUILDPLATFORM}
|
ARG BUILDPLATFORM=${BUILDPLATFORM}
|
||||||
|
|
||||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.3"]
|
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.1"]
|
||||||
ARG RUBY_VERSION="3.2.3"
|
ARG RUBY_VERSION="3.3.1"
|
||||||
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||||
ARG NODE_MAJOR_VERSION="20"
|
ARG NODE_MAJOR_VERSION="20"
|
||||||
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
||||||
ARG DEBIAN_VERSION="bookworm"
|
ARG DEBIAN_VERSION="bookworm"
|
||||||
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
||||||
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
|
FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node
|
||||||
# Ruby image to use for base image based on combined variables (ex: 3.2.3-slim-bookworm)
|
# Ruby image to use for base image based on combined variables (ex: 3.3.1-slim-bookworm)
|
||||||
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
|
FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
|
||||||
|
|
||||||
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
||||||
|
@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA=""
|
||||||
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
|
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
|
||||||
ARG RAILS_SERVE_STATIC_FILES="true"
|
ARG RAILS_SERVE_STATIC_FILES="true"
|
||||||
# Allow to use YJIT compiler
|
# Allow to use YJIT compiler
|
||||||
# See: https://github.com/ruby/ruby/blob/v3_2_3/doc/yjit/yjit.md
|
# See: https://github.com/ruby/ruby/blob/v3_2_4/doc/yjit/yjit.md
|
||||||
ARG RUBY_YJIT_ENABLE="1"
|
ARG RUBY_YJIT_ENABLE="1"
|
||||||
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
|
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
|
||||||
ARG TZ="Etc/UTC"
|
ARG TZ="Etc/UTC"
|
||||||
|
@ -205,7 +205,12 @@ ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
# Use Ruby on Rails to create Mastodon assets
|
# 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
|
# Cleanup temporary files
|
||||||
rm -fr /opt/mastodon/tmp;
|
rm -fr /opt/mastodon/tmp;
|
||||||
|
|
||||||
|
@ -257,4 +262,4 @@ USER mastodon
|
||||||
# Expose default Puma ports
|
# Expose default Puma ports
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
# Set container tini as default entry point
|
# Set container tini as default entry point
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
|
42
Gemfile
42
Gemfile
|
@ -1,28 +1,28 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '>= 3.0.0'
|
ruby '>= 3.1.0'
|
||||||
|
|
||||||
gem 'puma', '~> 6.3'
|
|
||||||
gem 'rails', '~> 7.1.1'
|
|
||||||
gem 'propshaft'
|
gem 'propshaft'
|
||||||
gem 'thor', '~> 1.2'
|
gem 'puma', '~> 6.3'
|
||||||
gem 'rack', '~> 2.2.7'
|
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
|
# For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182
|
||||||
gem 'irb', '~> 1.8'
|
gem 'irb', '~> 1.8'
|
||||||
|
|
||||||
|
gem 'dotenv'
|
||||||
gem 'haml-rails', '~>2.0'
|
gem 'haml-rails', '~>2.0'
|
||||||
gem 'pg', '~> 1.5'
|
gem 'pg', '~> 1.5'
|
||||||
gem 'pghero'
|
gem 'pghero'
|
||||||
gem 'dotenv-rails', '~> 2.8'
|
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||||
|
gem 'blurhash', '~> 0.1'
|
||||||
gem 'fog-core', '<= 2.4.0'
|
gem 'fog-core', '<= 2.4.0'
|
||||||
gem 'fog-openstack', '~> 1.0', require: false
|
gem 'fog-openstack', '~> 1.0', require: false
|
||||||
gem 'kt-paperclip', '~> 7.2'
|
gem 'kt-paperclip', '~> 7.2'
|
||||||
gem 'md-paperclip-azure', '~> 2.2', require: false
|
gem 'md-paperclip-azure', '~> 2.2', require: false
|
||||||
gem 'blurhash', '~> 0.1'
|
|
||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
|
@ -31,7 +31,7 @@ gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'chewy', '~> 7.3'
|
gem 'chewy', '~> 7.3'
|
||||||
gem 'devise', '~> 4.9'
|
gem 'devise', '~> 4.9'
|
||||||
gem 'devise-two-factor', '~> 4.1'
|
gem 'devise-two-factor'
|
||||||
|
|
||||||
group :pam_authentication, optional: true do
|
group :pam_authentication, optional: true do
|
||||||
gem 'devise_pam_authenticatable2', '~> 9.2'
|
gem 'devise_pam_authenticatable2', '~> 9.2'
|
||||||
|
@ -39,11 +39,11 @@ end
|
||||||
|
|
||||||
gem 'net-ldap', '~> 0.18'
|
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', '~> 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-rails_csrf_protection', '~> 1.0'
|
||||||
|
gem 'omniauth-saml', '~> 2.0'
|
||||||
|
|
||||||
gem 'color_diff', '~> 0.1'
|
gem 'color_diff', '~> 0.1'
|
||||||
gem 'csv', '~> 3.2'
|
gem 'csv', '~> 3.2'
|
||||||
|
@ -53,9 +53,8 @@ gem 'ed25519', '~> 1.3'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
gem 'redis-namespace', '~> 1.10'
|
|
||||||
gem 'htmlentities', '~> 4.3'
|
gem 'htmlentities', '~> 4.3'
|
||||||
gem 'http', '~> 5.1'
|
gem 'http', '~> 5.2.0'
|
||||||
gem 'http_accept_language', '~> 2.1'
|
gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'httplog', '~> 1.6.2'
|
gem 'httplog', '~> 1.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
|
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 'inline_svg'
|
||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
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 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
|
||||||
gem 'nokogiri', '~> 1.15'
|
gem 'nokogiri', '~> 1.15'
|
||||||
gem 'nsa'
|
gem 'nsa'
|
||||||
gem 'oj', '~> 3.14'
|
gem 'oj', '~> 3.14'
|
||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
gem 'parslet'
|
gem 'parslet'
|
||||||
gem 'posix-spawn'
|
gem 'premailer-rails'
|
||||||
gem 'public_suffix', '~> 5.0'
|
gem 'public_suffix', '~> 5.0'
|
||||||
gem 'pundit', '~> 2.3'
|
gem 'pundit', '~> 2.3'
|
||||||
gem 'premailer-rails'
|
|
||||||
gem 'rack-attack', '~> 6.6'
|
gem 'rack-attack', '~> 6.6'
|
||||||
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
|
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
|
||||||
gem 'rails-i18n', '~> 7.0'
|
gem 'rails-i18n', '~> 7.0'
|
||||||
gem 'redcarpet', '~> 3.6'
|
gem 'redcarpet', '~> 3.6'
|
||||||
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
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 'rqrcode', '~> 2.2'
|
||||||
gem 'ruby-progressbar', '~> 1.13'
|
gem 'ruby-progressbar', '~> 1.13'
|
||||||
gem 'sanitize', '~> 6.0'
|
gem 'sanitize', '~> 6.0'
|
||||||
gem 'scenic', '~> 1.7'
|
gem 'scenic', '~> 1.7'
|
||||||
gem 'sidekiq', '~> 6.5'
|
gem 'sidekiq', '~> 6.5'
|
||||||
|
gem 'sidekiq-bulk', '~> 0.2.0'
|
||||||
gem 'sidekiq-scheduler', '~> 5.0'
|
gem 'sidekiq-scheduler', '~> 5.0'
|
||||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||||
gem 'sidekiq-bulk', '~> 0.2.0'
|
|
||||||
gem 'simple-navigation', '~> 4.4'
|
|
||||||
gem 'simple_form', '~> 5.2'
|
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 'strong_migrations', '1.8.0'
|
||||||
gem 'tty-prompt', '~> 0.23', require: false
|
gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
gem 'tzinfo-data', '~> 1.2023'
|
gem 'tzinfo-data', '~> 1.2023'
|
||||||
|
gem 'webauthn', '~> 3.0'
|
||||||
gem 'webpacker', '~> 5.4'
|
gem 'webpacker', '~> 5.4'
|
||||||
gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9'
|
gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9'
|
||||||
gem 'webauthn', '~> 3.0'
|
|
||||||
|
|
||||||
gem 'json-ld'
|
gem 'json-ld'
|
||||||
gem 'json-ld-preloaded', '~> 3.2'
|
gem 'json-ld-preloaded', '~> 3.2'
|
||||||
|
@ -198,13 +197,14 @@ group :production do
|
||||||
gem 'lograge', '~> 0.12'
|
gem 'lograge', '~> 0.12'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
gem 'cocoon', '~> 1.2'
|
||||||
gem 'concurrent-ruby', require: false
|
gem 'concurrent-ruby', require: false
|
||||||
gem 'connection_pool', require: false
|
gem 'connection_pool', require: false
|
||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
|
|
||||||
gem 'cocoon', '~> 1.2'
|
|
||||||
|
|
||||||
gem 'net-http', '~> 0.4.0'
|
gem 'net-http', '~> 0.4.0'
|
||||||
gem 'rubyzip', '~> 2.3'
|
gem 'rubyzip', '~> 2.3'
|
||||||
|
|
||||||
gem 'hcaptcha', '~> 7.1'
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
|
||||||
|
gem 'mail', '~> 2.8'
|
||||||
|
|
187
Gemfile.lock
187
Gemfile.lock
|
@ -97,22 +97,20 @@ GEM
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
attr_encrypted (4.0.0)
|
attr_required (1.0.2)
|
||||||
encryptor (~> 3.0.0)
|
|
||||||
attr_required (1.0.1)
|
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.873.0)
|
aws-partitions (1.922.0)
|
||||||
aws-sdk-core (3.190.1)
|
aws-sdk-core (3.194.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.75.0)
|
aws-sdk-kms (1.80.0)
|
||||||
aws-sdk-core (~> 3, >= 3.188.0)
|
aws-sdk-core (~> 3, >= 3.193.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.142.0)
|
aws-sdk-s3 (1.149.1)
|
||||||
aws-sdk-core (~> 3, >= 3.189.0)
|
aws-sdk-core (~> 3, >= 3.194.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.8)
|
||||||
aws-sigv4 (1.8.0)
|
aws-sigv4 (1.8.0)
|
||||||
|
@ -132,7 +130,7 @@ GEM
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
rouge (>= 1.0.0)
|
rouge (>= 1.0.0)
|
||||||
better_html (2.0.2)
|
better_html (2.1.1)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
ast (~> 2.0)
|
ast (~> 2.0)
|
||||||
|
@ -140,9 +138,9 @@ GEM
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
bigdecimal (3.1.7)
|
bigdecimal (3.1.7)
|
||||||
bindata (2.4.15)
|
bindata (2.5.0)
|
||||||
binding_of_caller (1.0.0)
|
binding_of_caller (1.0.1)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 1.2.0)
|
||||||
blurhash (0.1.7)
|
blurhash (0.1.7)
|
||||||
bootsnap (1.18.3)
|
bootsnap (1.18.3)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
|
@ -167,11 +165,11 @@ GEM
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
case_transform (0.2)
|
case_transform (0.2)
|
||||||
activesupport
|
activesupport
|
||||||
cbor (0.5.9.6)
|
cbor (0.5.9.8)
|
||||||
charlock_holmes (0.7.7)
|
charlock_holmes (0.7.7)
|
||||||
chewy (7.5.1)
|
chewy (7.6.0)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
elasticsearch (>= 7.12.0, < 7.14.0)
|
elasticsearch (>= 7.14.0, < 8)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
climate_control (1.2.0)
|
climate_control (1.2.0)
|
||||||
|
@ -182,31 +180,30 @@ GEM
|
||||||
cose (1.3.0)
|
cose (1.3.0)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
openssl-signature_algorithm (~> 1.0)
|
openssl-signature_algorithm (~> 1.0)
|
||||||
crack (0.4.6)
|
crack (1.0.0)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.14.0)
|
css_parser (1.17.1)
|
||||||
addressable
|
addressable
|
||||||
csv (3.2.8)
|
csv (3.3.0)
|
||||||
database_cleaner-active_record (2.1.0)
|
database_cleaner-active_record (2.1.0)
|
||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.3.4)
|
date (3.3.4)
|
||||||
debug (1.9.1)
|
debug (1.9.2)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
debug_inspector (1.1.0)
|
debug_inspector (1.2.0)
|
||||||
devise (4.9.3)
|
devise (4.9.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (4.1.1)
|
devise-two-factor (5.0.0)
|
||||||
activesupport (~> 7.0)
|
activesupport (~> 7.0)
|
||||||
attr_encrypted (>= 1.3, < 5, != 2)
|
|
||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (~> 7.0)
|
railties (~> 7.0)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
|
@ -217,36 +214,31 @@ GEM
|
||||||
discard (1.3.0)
|
discard (1.3.0)
|
||||||
activerecord (>= 4.2, < 8)
|
activerecord (>= 4.2, < 8)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.6.20240107)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
|
||||||
doorkeeper (5.6.9)
|
doorkeeper (5.6.9)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.8.1)
|
dotenv (3.1.2)
|
||||||
dotenv-rails (2.8.1)
|
|
||||||
dotenv (= 2.8.1)
|
|
||||||
railties (>= 3.2)
|
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
ed25519 (1.3.0)
|
ed25519 (1.3.0)
|
||||||
elasticsearch (7.13.3)
|
elasticsearch (7.17.10)
|
||||||
elasticsearch-api (= 7.13.3)
|
elasticsearch-api (= 7.17.10)
|
||||||
elasticsearch-transport (= 7.13.3)
|
elasticsearch-transport (= 7.17.10)
|
||||||
elasticsearch-api (7.13.3)
|
elasticsearch-api (7.17.10)
|
||||||
multi_json
|
multi_json
|
||||||
elasticsearch-dsl (0.1.10)
|
elasticsearch-dsl (0.1.10)
|
||||||
elasticsearch-transport (7.13.3)
|
elasticsearch-transport (7.17.10)
|
||||||
faraday (~> 1)
|
faraday (>= 1, < 3)
|
||||||
multi_json
|
multi_json
|
||||||
email_spec (2.2.2)
|
email_spec (2.2.2)
|
||||||
htmlentities (~> 4.3.3)
|
htmlentities (~> 4.3.3)
|
||||||
launchy (~> 2.1)
|
launchy (~> 2.1)
|
||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
encryptor (3.0.0)
|
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
et-orbi (1.2.7)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.109.0)
|
excon (0.110.0)
|
||||||
fabrication (2.31.0)
|
fabrication (2.31.0)
|
||||||
faker (3.2.3)
|
faker (3.3.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
|
@ -274,10 +266,10 @@ GEM
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fast_blank (1.0.1)
|
fast_blank (1.0.1)
|
||||||
fastimage (2.3.0)
|
fastimage (2.3.1)
|
||||||
ffi (1.15.5)
|
ffi (1.16.3)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.3.2)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.15.5)
|
||||||
rake
|
rake
|
||||||
fog-core (2.4.0)
|
fog-core (2.4.0)
|
||||||
builder
|
builder
|
||||||
|
@ -291,7 +283,7 @@ GEM
|
||||||
fog-core (~> 2.1)
|
fog-core (~> 2.1)
|
||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
formatador (1.1.0)
|
formatador (1.1.0)
|
||||||
fugit (1.8.1)
|
fugit (1.10.1)
|
||||||
et-orbi (~> 1, >= 1.2.7)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
fuubar (2.5.1)
|
fuubar (2.5.1)
|
||||||
|
@ -308,7 +300,7 @@ GEM
|
||||||
activesupport (>= 5.1)
|
activesupport (>= 5.1)
|
||||||
haml (>= 4.0.6)
|
haml (>= 4.0.6)
|
||||||
railties (>= 5.1)
|
railties (>= 5.1)
|
||||||
haml_lint (0.57.0)
|
haml_lint (0.58.0)
|
||||||
haml (>= 5.0)
|
haml (>= 5.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
|
@ -318,15 +310,16 @@ GEM
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
hcaptcha (7.1.0)
|
hcaptcha (7.1.0)
|
||||||
json
|
json
|
||||||
highline (2.1.0)
|
highline (3.0.1)
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (5.1.1)
|
http (5.2.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
|
base64 (~> 0.1)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 2.2)
|
http-form_data (~> 2.2)
|
||||||
llhttp-ffi (~> 0.4.0)
|
llhttp-ffi (~> 0.5.0)
|
||||||
http-cookie (1.0.5)
|
http-cookie (1.0.5)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (2.3.0)
|
http-form_data (2.3.0)
|
||||||
|
@ -353,11 +346,11 @@ GEM
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
io-console (0.7.2)
|
io-console (0.7.2)
|
||||||
irb (1.12.0)
|
irb (1.13.1)
|
||||||
rdoc
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.7.1)
|
json (2.7.2)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.15.3.1)
|
json-jwt (1.15.3.1)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
|
@ -374,7 +367,7 @@ GEM
|
||||||
json-ld-preloaded (3.3.0)
|
json-ld-preloaded (3.3.0)
|
||||||
json-ld (~> 3.3)
|
json-ld (~> 3.3)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
json-schema (4.2.0)
|
json-schema (4.3.0)
|
||||||
addressable (>= 2.8)
|
addressable (>= 2.8)
|
||||||
jsonapi-renderer (0.2.2)
|
jsonapi-renderer (0.2.2)
|
||||||
jwt (2.7.1)
|
jwt (2.7.1)
|
||||||
|
@ -399,15 +392,15 @@ GEM
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
launchy (2.5.2)
|
launchy (2.5.2)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
letter_opener (1.10.0)
|
||||||
launchy (>= 2.2, < 3)
|
launchy (>= 2.2, < 4)
|
||||||
letter_opener_web (2.0.0)
|
letter_opener_web (2.0.0)
|
||||||
actionmailer (>= 5.2)
|
actionmailer (>= 5.2)
|
||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml
|
rexml
|
||||||
link_header (0.0.8)
|
link_header (0.0.8)
|
||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.5.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
lograge (0.14.0)
|
lograge (0.14.0)
|
||||||
|
@ -423,7 +416,7 @@ GEM
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.2)
|
marcel (1.0.4)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
|
@ -434,13 +427,13 @@ GEM
|
||||||
memory_profiler (1.0.1)
|
memory_profiler (1.0.1)
|
||||||
mime-types (3.5.2)
|
mime-types (3.5.2)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2023.1205)
|
mime-types-data (3.2024.0305)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.5)
|
mini_portile2 (2.8.6)
|
||||||
minitest (5.22.3)
|
minitest (5.22.3)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.4.0)
|
||||||
mutex_m (0.2.0)
|
mutex_m (0.2.0)
|
||||||
net-http (0.4.1)
|
net-http (0.4.1)
|
||||||
uri
|
uri
|
||||||
|
@ -454,10 +447,10 @@ GEM
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.4.0.1)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
nio4r (2.7.1)
|
||||||
nokogiri (1.16.3)
|
nokogiri (1.16.4)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.3.0)
|
nsa (0.3.0)
|
||||||
|
@ -501,7 +494,7 @@ GEM
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.18)
|
ox (2.14.18)
|
||||||
parallel (1.24.0)
|
parallel (1.24.0)
|
||||||
parser (3.3.0.5)
|
parser (3.3.1.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
|
@ -510,8 +503,7 @@ GEM
|
||||||
pg (1.5.6)
|
pg (1.5.6)
|
||||||
pghero (3.4.1)
|
pghero (3.4.1)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
posix-spawn (0.3.15)
|
premailer (1.23.0)
|
||||||
premailer (1.21.0)
|
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.12.0)
|
css_parser (>= 1.12.0)
|
||||||
htmlentities (>= 4.0.0)
|
htmlentities (>= 4.0.0)
|
||||||
|
@ -527,7 +519,7 @@ GEM
|
||||||
railties (>= 7.0.0)
|
railties (>= 7.0.0)
|
||||||
psych (5.1.2)
|
psych (5.1.2)
|
||||||
stringio
|
stringio
|
||||||
public_suffix (5.0.4)
|
public_suffix (5.0.5)
|
||||||
puma (6.4.2)
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.1)
|
pundit (2.3.1)
|
||||||
|
@ -548,7 +540,7 @@ GEM
|
||||||
rack-protection (3.2.0)
|
rack-protection (3.2.0)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (~> 2.2, >= 2.2.4)
|
rack (~> 2.2, >= 2.2.4)
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (1.0.2)
|
rack-session (1.0.2)
|
||||||
rack (< 3)
|
rack (< 3)
|
||||||
|
@ -594,7 +586,7 @@ GEM
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.1.0)
|
rake (13.2.1)
|
||||||
rdf (3.3.1)
|
rdf (3.3.1)
|
||||||
bcp47_spec (~> 0.2)
|
bcp47_spec (~> 0.2)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
|
@ -609,16 +601,16 @@ GEM
|
||||||
redlock (1.3.2)
|
redlock (1.3.2)
|
||||||
redis (>= 3.0.0, < 6.0)
|
redis (>= 3.0.0, < 6.0)
|
||||||
regexp_parser (2.9.0)
|
regexp_parser (2.9.0)
|
||||||
reline (0.4.3)
|
reline (0.5.5)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
request_store (1.5.1)
|
request_store (1.6.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.6)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.1.2)
|
rouge (4.2.1)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
|
@ -642,13 +634,13 @@ GEM
|
||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-sidekiq (4.1.0)
|
rspec-sidekiq (4.2.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
rspec-expectations (~> 3.0)
|
rspec-expectations (~> 3.0)
|
||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.13.1)
|
rspec-support (3.13.1)
|
||||||
rubocop (1.62.1)
|
rubocop (1.63.4)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
@ -659,27 +651,30 @@ GEM
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.31.2)
|
rubocop-ast (1.31.3)
|
||||||
parser (>= 3.3.0.4)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-capybara (2.20.0)
|
rubocop-capybara (2.20.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.25.1)
|
rubocop-factory_bot (2.25.1)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-performance (1.20.2)
|
rubocop-performance (1.21.0)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.30.0, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.24.0)
|
rubocop-rails (2.24.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (2.27.1)
|
rubocop-rspec (2.29.2)
|
||||||
rubocop (~> 1.40)
|
rubocop (~> 1.40)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
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-prof (1.7.0)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-saml (1.15.0)
|
ruby-saml (1.16.0)
|
||||||
nokogiri (>= 1.13.10)
|
nokogiri (>= 1.13.10)
|
||||||
rexml
|
rexml
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
|
@ -691,10 +686,10 @@ GEM
|
||||||
sanitize (6.1.0)
|
sanitize (6.1.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.7.0)
|
scenic (1.8.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
selenium-webdriver (4.18.1)
|
selenium-webdriver (4.20.1)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 3.0)
|
rubyzip (>= 1.2.2, < 3.0)
|
||||||
|
@ -731,7 +726,7 @@ GEM
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
stackprof (0.2.26)
|
stackprof (0.2.26)
|
||||||
statsd-ruby (1.5.0)
|
statsd-ruby (1.5.0)
|
||||||
stoplight (3.0.2)
|
stoplight (4.1.0)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
stringio (3.1.0)
|
stringio (3.1.0)
|
||||||
strong_migrations (1.8.0)
|
strong_migrations (1.8.0)
|
||||||
|
@ -746,7 +741,7 @@ GEM
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (1.0.1)
|
terrapin (1.0.1)
|
||||||
climate_control
|
climate_control
|
||||||
test-prof (1.3.2)
|
test-prof (1.3.3)
|
||||||
thor (1.3.1)
|
thor (1.3.1)
|
||||||
tilt (2.3.0)
|
tilt (2.3.0)
|
||||||
timeout (0.4.1)
|
timeout (0.4.1)
|
||||||
|
@ -763,7 +758,7 @@ GEM
|
||||||
tty-cursor (~> 0.7)
|
tty-cursor (~> 0.7)
|
||||||
tty-screen (~> 0.8)
|
tty-screen (~> 0.8)
|
||||||
wisper (~> 2.0)
|
wisper (~> 2.0)
|
||||||
tty-screen (0.8.1)
|
tty-screen (0.8.2)
|
||||||
twitter-text (3.1.0)
|
twitter-text (3.1.0)
|
||||||
idn-ruby
|
idn-ruby
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
|
@ -773,9 +768,9 @@ GEM
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.9.1)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
uri (0.12.2)
|
uri (0.13.0)
|
||||||
validate_email (0.1.6)
|
validate_email (0.1.6)
|
||||||
activemodel (>= 3.0)
|
activemodel (>= 3.0)
|
||||||
mail (>= 2.2.5)
|
mail (>= 2.2.5)
|
||||||
|
@ -796,7 +791,7 @@ GEM
|
||||||
webfinger (1.2.0)
|
webfinger (1.2.0)
|
||||||
activesupport
|
activesupport
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
webmock (3.22.0)
|
webmock (3.23.0)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
|
@ -843,11 +838,11 @@ DEPENDENCIES
|
||||||
database_cleaner-active_record
|
database_cleaner-active_record
|
||||||
debug (~> 1.8)
|
debug (~> 1.8)
|
||||||
devise (~> 4.9)
|
devise (~> 4.9)
|
||||||
devise-two-factor (~> 4.1)
|
devise-two-factor
|
||||||
devise_pam_authenticatable2 (~> 9.2)
|
devise_pam_authenticatable2 (~> 9.2)
|
||||||
discard (~> 1.2)
|
discard (~> 1.2)
|
||||||
doorkeeper (~> 5.6)
|
doorkeeper (~> 5.6)
|
||||||
dotenv-rails (~> 2.8)
|
dotenv
|
||||||
ed25519 (~> 1.3)
|
ed25519 (~> 1.3)
|
||||||
email_spec
|
email_spec
|
||||||
fabrication (~> 2.30)
|
fabrication (~> 2.30)
|
||||||
|
@ -862,7 +857,7 @@ DEPENDENCIES
|
||||||
hcaptcha (~> 7.1)
|
hcaptcha (~> 7.1)
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
http (~> 5.1)
|
http (~> 5.2.0)
|
||||||
http_accept_language (~> 2.1)
|
http_accept_language (~> 2.1)
|
||||||
httplog (~> 1.6.2)
|
httplog (~> 1.6.2)
|
||||||
i18n (= 1.14.1)
|
i18n (= 1.14.1)
|
||||||
|
@ -879,6 +874,7 @@ DEPENDENCIES
|
||||||
letter_opener_web (~> 2.0)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
|
mail (~> 2.8)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
md-paperclip-azure (~> 2.2)
|
md-paperclip-azure (~> 2.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
|
@ -897,7 +893,6 @@ DEPENDENCIES
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
pghero
|
pghero
|
||||||
posix-spawn
|
|
||||||
premailer-rails
|
premailer-rails
|
||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
propshaft
|
propshaft
|
||||||
|
@ -939,7 +934,7 @@ DEPENDENCIES
|
||||||
simplecov (~> 0.22)
|
simplecov (~> 0.22)
|
||||||
simplecov-lcov (~> 0.8)
|
simplecov-lcov (~> 0.8)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 3.0.1)
|
stoplight (~> 4.1)
|
||||||
strong_migrations (= 1.8.0)
|
strong_migrations (= 1.8.0)
|
||||||
test-prof
|
test-prof
|
||||||
thor (~> 1.2)
|
thor (~> 1.2)
|
||||||
|
@ -953,7 +948,7 @@ DEPENDENCIES
|
||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.2.2p53
|
ruby 3.3.1p55
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.4
|
2.5.9
|
||||||
|
|
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
|
# 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: 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: 4000, host: 4000
|
||||||
config.vm.network :forwarded_port, guest: 8080, host: 8080
|
config.vm.network :forwarded_port, guest: 8080, host: 8080
|
||||||
config.vm.network :forwarded_port, guest: 9200, host: 9200
|
config.vm.network :forwarded_port, guest: 9200, host: 9200
|
||||||
|
|
|
@ -46,7 +46,7 @@ class AccountsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_statuses
|
def default_statuses
|
||||||
@account.statuses.not_local_only.where(visibility: [:public, :unlisted])
|
@account.statuses.not_local_only.distributable_visibility
|
||||||
end
|
end
|
||||||
|
|
||||||
def only_media_scope
|
def only_media_scope
|
||||||
|
|
|
@ -31,7 +31,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
||||||
|
|
||||||
def set_replies
|
def set_replies
|
||||||
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id).joins(:account).merge(Account.without_suspended) : @account.statuses
|
@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])
|
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ module Admin
|
||||||
|
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
|
@ -19,10 +18,6 @@ module Admin
|
||||||
@body_classes = 'admin'
|
@body_classes = 'admin'
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,6 +25,8 @@ class Admin::DomainAllowsController < Admin::BaseController
|
||||||
def destroy
|
def destroy
|
||||||
authorize @domain_allow, :destroy?
|
authorize @domain_allow, :destroy?
|
||||||
UnallowDomainService.new.call(@domain_allow)
|
UnallowDomainService.new.call(@domain_allow)
|
||||||
|
log_action :destroy, @domain_allow
|
||||||
|
|
||||||
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
|
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Admin
|
||||||
|
|
||||||
@site_upload.destroy!
|
@site_upload.destroy!
|
||||||
|
|
||||||
redirect_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
|
redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -9,6 +9,7 @@ class Api::BaseController < ApplicationController
|
||||||
include Api::CachingConcern
|
include Api::CachingConcern
|
||||||
include Api::ContentSecurityPolicy
|
include Api::ContentSecurityPolicy
|
||||||
include Api::ErrorHandling
|
include Api::ErrorHandling
|
||||||
|
include Api::Pagination
|
||||||
|
|
||||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||||
|
|
||||||
|
@ -29,21 +30,6 @@ class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
protected
|
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)
|
def limit_param(default_limit)
|
||||||
return default_limit unless params[: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?
|
render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.unavailable?
|
||||||
end
|
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!
|
def require_user!
|
||||||
if !current_user
|
if !current_user
|
||||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||||
|
@ -104,14 +86,6 @@ class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
private
|
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)
|
def respond_with_error(code)
|
||||||
render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code
|
render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Accounts::CredentialsController < Api::BaseController
|
class Api::V1::Accounts::CredentialsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:update]
|
before_action -> { doorkeeper_authorize! :read, :'read:accounts', :'read:me' }, except: [:update]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update]
|
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|
||||||
|
|
|
@ -9,16 +9,22 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }, only: [:block, :unblock]
|
before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }, only: [:block, :unblock]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
||||||
|
|
||||||
before_action :require_user!, except: [:show, :create]
|
before_action :require_user!, except: [:index, :show, :create]
|
||||||
before_action :set_account, except: [:create]
|
before_action :set_account, except: [:index, :create]
|
||||||
before_action :check_account_approval, except: [:create]
|
before_action :set_accounts, only: [:index]
|
||||||
before_action :check_account_confirmation, except: [:create]
|
before_action :check_account_approval, except: [:index, :create]
|
||||||
|
before_action :check_account_confirmation, except: [:index, :create]
|
||||||
before_action :check_enabled_registrations, only: [:create]
|
before_action :check_enabled_registrations, only: [:create]
|
||||||
|
before_action :check_accounts_limit, only: [:index]
|
||||||
|
|
||||||
skip_before_action :require_authenticated_user!, only: :create
|
skip_before_action :require_authenticated_user!, only: :create
|
||||||
|
|
||||||
override_rate_limit_headers :follow, family: :follows
|
override_rate_limit_headers :follow, family: :follows
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
cache_if_unauthenticated!
|
cache_if_unauthenticated!
|
||||||
render json: @account, serializer: REST::AccountSerializer
|
render json: @account, serializer: REST::AccountSerializer
|
||||||
|
@ -79,6 +85,10 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
@account = Account.find(params[:id])
|
@account = Account.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_accounts
|
||||||
|
@accounts = Account.where(id: account_ids).without_unapproved
|
||||||
|
end
|
||||||
|
|
||||||
def check_account_approval
|
def check_account_approval
|
||||||
raise(ActiveRecord::RecordNotFound) if @account.local? && @account.user_pending?
|
raise(ActiveRecord::RecordNotFound) if @account.local? && @account.user_pending?
|
||||||
end
|
end
|
||||||
|
@ -87,10 +97,22 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
raise(ActiveRecord::RecordNotFound) if @account.local? && !@account.user_confirmed?
|
raise(ActiveRecord::RecordNotFound) if @account.local? && !@account.user_confirmed?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_accounts_limit
|
||||||
|
raise(Mastodon::ValidationError) if account_ids.size > DEFAULT_ACCOUNTS_LIMIT
|
||||||
|
end
|
||||||
|
|
||||||
def relationships(**options)
|
def relationships(**options)
|
||||||
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
|
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def account_ids
|
||||||
|
Array(accounts_params[:ids]).uniq.map(&:to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def accounts_params
|
||||||
|
params.permit(ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
|
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,10 +29,11 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
def create
|
def create
|
||||||
authorize :domain_block, :create?
|
authorize :domain_block, :create?
|
||||||
|
|
||||||
|
@domain_block = DomainBlock.new(resource_params)
|
||||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
||||||
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if existing_domain_block.present?
|
return render json: existing_domain_block, serializer: REST::Admin::ExistingDomainBlockErrorSerializer, status: 422 if conflicts_with_existing_block?(@domain_block, existing_domain_block)
|
||||||
|
|
||||||
@domain_block = DomainBlock.create!(resource_params)
|
@domain_block.save!
|
||||||
DomainBlockWorker.perform_async(@domain_block.id)
|
DomainBlockWorker.perform_async(@domain_block.id)
|
||||||
log_action :create, @domain_block
|
log_action :create, @domain_block
|
||||||
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer
|
||||||
|
@ -55,6 +56,10 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def conflicts_with_existing_block?(domain_block, existing_domain_block)
|
||||||
|
existing_domain_block.present? && (existing_domain_block.domain == TagManager.instance.normalize_domain(domain_block.domain) || !domain_block.stricter_than?(existing_domain_block))
|
||||||
|
end
|
||||||
|
|
||||||
def set_domain_blocks
|
def set_domain_blocks
|
||||||
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,10 +12,6 @@ class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_recently_used_tags
|
def set_recently_used_tags
|
||||||
@recently_used_tags = Tag.recently_used(current_account).where.not(id: featured_tag_ids).limit(10)
|
@recently_used_tags = Tag.suggestions_for_account(current_account).limit(10)
|
||||||
end
|
|
||||||
|
|
||||||
def featured_tag_ids
|
|
||||||
current_account.featured_tags.pluck(:tag_id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Push::SubscriptionsController < Api::BaseController
|
class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
|
include Redisable
|
||||||
|
include Lockable
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :push }
|
before_action -> { doorkeeper_authorize! :push }
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_push_subscription
|
before_action :set_push_subscription, only: [:show, :update]
|
||||||
before_action :check_push_subscription, only: [:show, :update]
|
before_action :check_push_subscription, only: [:show, :update]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -11,16 +14,18 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@push_subscription&.destroy!
|
with_redis_lock("push_subscription:#{current_user.id}") do
|
||||||
|
destroy_web_push_subscriptions!
|
||||||
|
|
||||||
@push_subscription = Web::PushSubscription.create!(
|
@push_subscription = Web::PushSubscription.create!(
|
||||||
endpoint: subscription_params[:endpoint],
|
endpoint: subscription_params[:endpoint],
|
||||||
key_p256dh: subscription_params[:keys][:p256dh],
|
key_p256dh: subscription_params[:keys][:p256dh],
|
||||||
key_auth: subscription_params[:keys][:auth],
|
key_auth: subscription_params[:keys][:auth],
|
||||||
data: data_params,
|
data: data_params,
|
||||||
user_id: current_user.id,
|
user_id: current_user.id,
|
||||||
access_token_id: doorkeeper_token.id
|
access_token_id: doorkeeper_token.id
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
|
||||||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
end
|
end
|
||||||
|
@ -31,14 +36,18 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@push_subscription&.destroy!
|
destroy_web_push_subscriptions!
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def destroy_web_push_subscriptions!
|
||||||
|
doorkeeper_token.web_push_subscriptions.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
def set_push_subscription
|
def set_push_subscription
|
||||||
@push_subscription = Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
@push_subscription = doorkeeper_token.web_push_subscriptions.first
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_push_subscription
|
def check_push_subscription
|
||||||
|
|
|
@ -23,7 +23,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::V1::Statuses::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginated_statuses
|
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),
|
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||||
params[:max_id],
|
params[:max_id],
|
||||||
params[:since_id]
|
params[:since_id]
|
||||||
|
|
|
@ -5,9 +5,11 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||||
before_action :require_user!, except: [:show, :context]
|
before_action :require_user!, except: [:index, :show, :context]
|
||||||
before_action :set_status, only: [:show, :context]
|
before_action :set_statuses, only: [:index]
|
||||||
before_action :set_thread, only: [:create]
|
before_action :set_status, only: [:show, :context]
|
||||||
|
before_action :set_thread, only: [:create]
|
||||||
|
before_action :check_statuses_limit, only: [:index]
|
||||||
|
|
||||||
override_rate_limit_headers :create, family: :statuses
|
override_rate_limit_headers :create, family: :statuses
|
||||||
override_rate_limit_headers :update, family: :statuses
|
override_rate_limit_headers :update, family: :statuses
|
||||||
|
@ -23,6 +25,11 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
DESCENDANTS_LIMIT = 60
|
DESCENDANTS_LIMIT = 60
|
||||||
DESCENDANTS_DEPTH_LIMIT = 20
|
DESCENDANTS_DEPTH_LIMIT = 20
|
||||||
|
|
||||||
|
def index
|
||||||
|
@statuses = cache_collection(@statuses, Status)
|
||||||
|
render json: @statuses, each_serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
cache_if_unauthenticated!
|
cache_if_unauthenticated!
|
||||||
@status = cache_collection([@status], Status).first
|
@status = cache_collection([@status], Status).first
|
||||||
|
@ -113,6 +120,10 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_statuses
|
||||||
|
@statuses = Status.permitted_statuses_from_ids(status_ids, current_account)
|
||||||
|
end
|
||||||
|
|
||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:id])
|
@status = Status.find(params[:id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
|
@ -127,6 +138,18 @@ class Api::V1::StatusesController < Api::BaseController
|
||||||
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
|
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_statuses_limit
|
||||||
|
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
|
||||||
|
end
|
||||||
|
|
||||||
|
def status_ids
|
||||||
|
Array(statuses_params[:ids]).uniq.map(&:to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def statuses_params
|
||||||
|
params.permit(ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
def status_params
|
def status_params
|
||||||
params.permit(
|
params.permit(
|
||||||
:status,
|
:status,
|
||||||
|
|
|
@ -19,6 +19,7 @@ class ApplicationController < ActionController::Base
|
||||||
helper_method :current_session
|
helper_method :current_session
|
||||||
helper_method :current_flavour
|
helper_method :current_flavour
|
||||||
helper_method :current_skin
|
helper_method :current_skin
|
||||||
|
helper_method :current_theme
|
||||||
helper_method :single_user_mode?
|
helper_method :single_user_mode?
|
||||||
helper_method :use_seamless_external_login?
|
helper_method :use_seamless_external_login?
|
||||||
helper_method :omniauth_only?
|
helper_method :omniauth_only?
|
||||||
|
@ -164,10 +165,7 @@ class ApplicationController < ActionController::Base
|
||||||
|
|
||||||
def respond_with_error(code)
|
def respond_with_error(code)
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.any do
|
format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] }
|
||||||
use_pack 'error'
|
|
||||||
render "errors/#{code}", layout: 'error', status: code, formats: [:html]
|
|
||||||
end
|
|
||||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
|
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -176,10 +174,7 @@ class ApplicationController < ActionController::Base
|
||||||
return unless self_destruct?
|
return unless self_destruct?
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.any do
|
format.any { render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html] }
|
||||||
use_pack 'error'
|
|
||||||
render 'errors/self_destruct', layout: 'auth', status: 410, formats: [:html]
|
|
||||||
end
|
|
||||||
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 }
|
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[410] }, status: 410 }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,6 @@ class Auth::ChallengesController < ApplicationController
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
before_action :set_pack
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
skip_before_action :check_self_destruct!
|
skip_before_action :check_self_destruct!
|
||||||
|
@ -21,10 +20,4 @@ class Auth::ChallengesController < ApplicationController
|
||||||
render_challenge
|
render_challenge
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'auth'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
|
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
|
||||||
before_action :redirect_confirmed_user, if: :signed_in_confirmed_user?
|
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?
|
@confirmation_user.nil? || @confirmation_user.confirmed?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'auth'
|
|
||||||
end
|
|
||||||
|
|
||||||
def redirect_confirmed_user
|
def redirect_confirmed_user
|
||||||
redirect_to(current_user.approved? ? root_path : edit_user_registration_path)
|
redirect_to(current_user.approved? ? root_path : edit_user_registration_path)
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
class Auth::PasswordsController < Devise::PasswordsController
|
class Auth::PasswordsController < Devise::PasswordsController
|
||||||
skip_before_action :check_self_destruct!
|
skip_before_action :check_self_destruct!
|
||||||
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
|
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
@ -32,8 +31,4 @@ class Auth::PasswordsController < Devise::PasswordsController
|
||||||
def reset_password_token_is_valid?
|
def reset_password_token_is_valid?
|
||||||
resource_class.with_reset_password_token(params[:reset_password_token]).present?
|
resource_class.with_reset_password_token(params[:reset_password_token]).present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'auth'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
before_action :set_invite, only: [:new, :create]
|
before_action :set_invite, only: [:new, :create]
|
||||||
before_action :check_enabled_registrations, only: [:new, :create]
|
before_action :check_enabled_registrations, only: [:new, :create]
|
||||||
before_action :configure_sign_up_params, only: [:create]
|
before_action :configure_sign_up_params, only: [:create]
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_sessions, only: [:edit, :update]
|
before_action :set_sessions, only: [:edit, :update]
|
||||||
before_action :set_strikes, only: [:edit, :update]
|
before_action :set_strikes, only: [:edit, :update]
|
||||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
||||||
|
@ -97,10 +96,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
|
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
skip_before_action :update_user_sign_in
|
skip_before_action :update_user_sign_in
|
||||||
|
|
||||||
prepend_before_action :set_pack
|
|
||||||
prepend_before_action :check_suspicious!, only: [:create]
|
prepend_before_action :check_suspicious!, only: [:create]
|
||||||
|
|
||||||
include Auth::TwoFactorAuthenticationConcern
|
include Auth::TwoFactorAuthenticationConcern
|
||||||
|
@ -104,10 +103,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'auth'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'lighter'
|
@body_classes = 'lighter'
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
class Auth::SetupController < ApplicationController
|
class Auth::SetupController < ApplicationController
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
before_action :set_pack
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :require_unconfirmed_or_pending!
|
before_action :require_unconfirmed_or_pending!
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
@ -43,8 +42,4 @@ class Auth::SetupController < ApplicationController
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email)
|
params.require(:user).permit(:email)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'sign_up'
|
|
||||||
end
|
|
||||||
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)
|
def prompt_for_two_factor(user)
|
||||||
register_attempt_in_session(user)
|
register_attempt_in_session(user)
|
||||||
|
|
||||||
use_pack 'auth'
|
|
||||||
|
|
||||||
@body_classes = 'lighter'
|
@body_classes = 'lighter'
|
||||||
@webauthn_enabled = user.webauthn_enabled?
|
@webauthn_enabled = user.webauthn_enabled?
|
||||||
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
|
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
|
||||||
|
|
|
@ -46,27 +46,19 @@ module CacheConcern
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Rename this method, as it does not perform any caching anymore.
|
||||||
def cache_collection(raw, klass)
|
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)
|
records = raw.to_a
|
||||||
return [] if raw.empty?
|
|
||||||
|
|
||||||
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
|
records
|
||||||
|
|
||||||
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] }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Rename this method, as it does not perform any caching anymore.
|
||||||
def cache_collection_paginated_by_id(raw, klass, limit, options)
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -66,7 +66,7 @@ module SignatureVerification
|
||||||
compare_signed_string = build_signed_string(include_query_string: false)
|
compare_signed_string = build_signed_string(include_query_string: false)
|
||||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
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?
|
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
||||||
|
|
||||||
|
@ -226,10 +226,10 @@ module SignatureVerification
|
||||||
end
|
end
|
||||||
|
|
||||||
if key_id.start_with?('acct:')
|
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)
|
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||||
account = ActivityPub::TagManager.instance.uri_to_actor(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
|
account
|
||||||
end
|
end
|
||||||
rescue Mastodon::PrivateNetworkAddressError => e
|
rescue Mastodon::PrivateNetworkAddressError => e
|
||||||
|
@ -238,12 +238,11 @@ module SignatureVerification
|
||||||
raise SignatureVerificationError, e.message
|
raise SignatureVerificationError, e.message
|
||||||
end
|
end
|
||||||
|
|
||||||
def stoplight_wrap_request(&block)
|
def stoplight_wrapper
|
||||||
Stoplight("source:#{request.remote_ip}", &block)
|
Stoplight("source:#{request.remote_ip}")
|
||||||
.with_threshold(1)
|
.with_threshold(1)
|
||||||
.with_cool_off_time(5.minutes.seconds)
|
.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) }
|
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
|
||||||
.run
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def actor_refresh_key!(actor)
|
def actor_refresh_key!(actor)
|
||||||
|
|
|
@ -3,87 +3,22 @@
|
||||||
module ThemingConcern
|
module ThemingConcern
|
||||||
extend ActiveSupport::Concern
|
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
|
private
|
||||||
|
|
||||||
def current_flavour
|
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
|
end
|
||||||
|
|
||||||
def current_skin
|
def current_skin
|
||||||
skins = Themes.instance.skins_for(current_flavour)
|
@current_skin ||= begin
|
||||||
[current_user&.setting_skin, Setting.skin, 'default'].find { |skin| skins.include?(skin) }
|
skins = Themes.instance.skins_for(current_flavour)
|
||||||
end
|
[current_user&.setting_skin, Setting.skin, 'system', 'default'].find { |skin| skins.include?(skin) }
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
pack_data
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_pack(data, pack_name, skin)
|
def current_theme
|
||||||
return pack(data, pack_name, skin) if valid_pack_data?(data, pack_name)
|
# NOTE: this is slightly different from upstream, as it's a derived value used
|
||||||
return if data['name'].blank?
|
# for the sole purpose of pointing to the appropriate stylesheet pack
|
||||||
|
[current_flavour, current_skin]
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,6 @@ module WebAppControllerConcern
|
||||||
vary_by 'Accept, Accept-Language, Cookie'
|
vary_by 'Accept, Accept-Language, Cookie'
|
||||||
|
|
||||||
before_action :redirect_unauthenticated_to_permalinks!
|
before_action :redirect_unauthenticated_to_permalinks!
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_app_body_class
|
before_action :set_app_body_class
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,8 +36,4 @@ module WebAppControllerConcern
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'home'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,15 +9,10 @@ class Disputes::BaseController < ApplicationController
|
||||||
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'admin'
|
@body_classes = 'admin'
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_filter
|
before_action :set_filter
|
||||||
before_action :set_status_filters
|
before_action :set_status_filters
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
|
@ -27,10 +26,6 @@ class Filters::StatusesController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_filter
|
def set_filter
|
||||||
@filter = current_account.custom_filters.find(params[:filter_id])
|
@filter = current_account.custom_filters.find(params[:filter_id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,6 @@ class FiltersController < ApplicationController
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_filter, only: [:edit, :update, :destroy]
|
before_action :set_filter, only: [:edit, :update, :destroy]
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
|
@ -45,10 +44,6 @@ class FiltersController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'settings'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_filter
|
def set_filter
|
||||||
@filter = current_account.custom_filters.find(params[:id])
|
@filter = current_account.custom_filters.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,6 @@ class InvitesController < ApplicationController
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
|
@ -40,10 +39,6 @@ class InvitesController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'settings'
|
|
||||||
end
|
|
||||||
|
|
||||||
def invites
|
def invites
|
||||||
current_user.invites.order(id: :desc)
|
current_user.invites.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,6 @@ class MediaController < ApplicationController
|
||||||
before_action :verify_permitted_status!
|
before_action :verify_permitted_status!
|
||||||
before_action :check_playable, only: :player
|
before_action :check_playable, only: :player
|
||||||
before_action :allow_iframing, only: :player
|
before_action :allow_iframing, only: :player
|
||||||
before_action :set_pack, only: :player
|
|
||||||
|
|
||||||
content_security_policy only: :player do |policy|
|
content_security_policy only: :player do |policy|
|
||||||
policy.frame_ancestors(false)
|
policy.frame_ancestors(false)
|
||||||
|
@ -48,8 +47,4 @@ class MediaController < ApplicationController
|
||||||
def allow_iframing
|
def allow_iframing
|
||||||
response.headers.delete('X-Frame-Options')
|
response.headers.delete('X-Frame-Options')
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'public'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||||
|
|
||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
before_action :authenticate_resource_owner!
|
before_action :authenticate_resource_owner!
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
content_security_policy do |p|
|
content_security_policy do |p|
|
||||||
|
@ -20,10 +19,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||||
store_location_for(:user, request.url)
|
store_location_for(:user, request.url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'auth'
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_success
|
def render_success
|
||||||
if skip_authorization? || (matching_token? && !truthy_param?('force_login'))
|
if skip_authorization? || (matching_token? && !truthy_param?('force_login'))
|
||||||
redirect_or_render authorize_response
|
redirect_or_render authorize_response
|
||||||
|
|
|
@ -5,7 +5,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
|
|
||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
before_action :authenticate_resource_owner!
|
before_action :authenticate_resource_owner!
|
||||||
before_action :set_pack
|
|
||||||
before_action :require_not_suspended!, only: :destroy
|
before_action :require_not_suspended!, only: :destroy
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
@ -31,10 +30,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
store_location_for(:user, request.url)
|
store_location_for(:user, request.url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'settings'
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_not_suspended!
|
def require_not_suspended!
|
||||||
forbidden if current_account.unavailable?
|
forbidden if current_account.unavailable?
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
class Redirect::BaseController < ApplicationController
|
class Redirect::BaseController < ApplicationController
|
||||||
vary_by 'Accept-Language'
|
vary_by 'Accept-Language'
|
||||||
|
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_resource
|
before_action :set_resource
|
||||||
before_action :set_app_body_class
|
before_action :set_app_body_class
|
||||||
|
|
||||||
|
@ -22,8 +21,4 @@ class Redirect::BaseController < ApplicationController
|
||||||
def set_resource
|
def set_resource
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'public'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,6 @@ class RelationshipsController < ApplicationController
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_accounts, only: :show
|
before_action :set_accounts, only: :show
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_relationships, only: :show
|
before_action :set_relationships, only: :show
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
@ -73,10 +72,6 @@ class RelationshipsController < ApplicationController
|
||||||
@body_classes = 'admin'
|
@body_classes = 'admin'
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::BaseController < ApplicationController
|
class Settings::BaseController < ApplicationController
|
||||||
before_action :set_pack
|
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
@ -10,10 +9,6 @@ class Settings::BaseController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'settings'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'admin'
|
@body_classes = 'admin'
|
||||||
end
|
end
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_recently_used_tags
|
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
|
end
|
||||||
|
|
||||||
def featured_tag_params
|
def featured_tag_params
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Settings::ImportsController < Settings::BaseController
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def failures
|
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|
|
respond_to do |format|
|
||||||
format.csv do
|
format.csv do
|
||||||
|
@ -92,7 +92,7 @@ class Settings::ImportsController < Settings::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_bulk_import
|
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
|
end
|
||||||
|
|
||||||
def set_recent_imports
|
def set_recent_imports
|
||||||
|
|
|
@ -7,10 +7,4 @@ class Settings::LoginActivitiesController < Settings::BaseController
|
||||||
def index
|
def index
|
||||||
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
|
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'settings'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,10 +85,6 @@ module Settings
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'auth'
|
|
||||||
end
|
|
||||||
|
|
||||||
def redirect_invalid_otp
|
def redirect_invalid_otp
|
||||||
flash[:error] = t('webauthn_credentials.otp_required')
|
flash[:error] = t('webauthn_credentials.otp_required')
|
||||||
redirect_to settings_two_factor_authentication_methods_path
|
redirect_to settings_two_factor_authentication_methods_path
|
||||||
|
|
|
@ -4,17 +4,12 @@ class SharesController < ApplicationController
|
||||||
layout 'modal'
|
layout 'modal'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'share'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = 'modal-layout compose-standalone'
|
@body_classes = 'modal-layout compose-standalone'
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,6 @@ class StatusesCleanupController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_policy
|
before_action :set_policy
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_pack
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
@ -27,10 +26,6 @@ class StatusesCleanupController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_pack
|
|
||||||
use_pack 'settings'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_policy
|
def set_policy
|
||||||
@policy = current_account.statuses_cleanup_policy || current_account.build_statuses_cleanup_policy(enabled: false)
|
@policy = current_account.statuses_cleanup_policy || current_account.build_statuses_cleanup_policy(enabled: false)
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,7 +41,6 @@ class StatusesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def embed
|
def embed
|
||||||
use_pack 'embed'
|
|
||||||
return not_found if @status.hidden? || @status.reblog?
|
return not_found if @status.hidden? || @status.reblog?
|
||||||
|
|
||||||
expires_in 180, public: true
|
expires_in 180, public: true
|
||||||
|
|
23
app/controllers/well_known/oauth_metadata_controller.rb
Normal file
23
app/controllers/well_known/oauth_metadata_controller.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WellKnown
|
||||||
|
class OauthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
||||||
|
include CacheConcern
|
||||||
|
|
||||||
|
# Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user`
|
||||||
|
# and thus re-issuing session cookies
|
||||||
|
serialization_scope nil
|
||||||
|
|
||||||
|
def show
|
||||||
|
# Due to this document potentially changing between Mastodon versions (as
|
||||||
|
# new OAuth scopes are added), we don't use expires_in to cache upstream,
|
||||||
|
# instead just caching in the rails cache:
|
||||||
|
render_with_cache(
|
||||||
|
json: ::OauthMetadataPresenter.new,
|
||||||
|
serializer: ::OauthMetadataSerializer,
|
||||||
|
content_type: 'application/json',
|
||||||
|
expires_in: 15.minutes
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -113,6 +113,14 @@ module ApplicationHelper
|
||||||
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
|
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def material_symbol(icon, attributes = {})
|
||||||
|
inline_svg_tag(
|
||||||
|
"400-24px/#{icon}.svg",
|
||||||
|
class: %w(icon).concat(attributes[:class].to_s.split),
|
||||||
|
role: :img
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def check_icon
|
def check_icon
|
||||||
inline_svg_tag 'check.svg'
|
inline_svg_tag 'check.svg'
|
||||||
end
|
end
|
||||||
|
@ -233,6 +241,32 @@ module ApplicationHelper
|
||||||
EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
|
EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def site_icon_path(type, size = '48')
|
||||||
|
icon = SiteUpload.find_by(var: type)
|
||||||
|
return nil unless icon
|
||||||
|
|
||||||
|
icon.file.url(size)
|
||||||
|
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
|
private
|
||||||
|
|
||||||
def storage_host_var
|
def storage_host_var
|
||||||
|
|
|
@ -19,6 +19,6 @@ module BrandingHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_logo
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,13 +49,11 @@ module ContextHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialized_context(named_contexts_map, context_extensions_map)
|
def serialized_context(named_contexts_map, context_extensions_map)
|
||||||
context_array = []
|
|
||||||
|
|
||||||
named_contexts = named_contexts_map.keys
|
named_contexts = named_contexts_map.keys
|
||||||
context_extensions = context_extensions_map.keys
|
context_extensions = context_extensions_map.keys
|
||||||
|
|
||||||
named_contexts.each do |key|
|
context_array = named_contexts.map do |key|
|
||||||
context_array << NAMED_CONTEXT_MAP[key]
|
NAMED_CONTEXT_MAP[key]
|
||||||
end
|
end
|
||||||
|
|
||||||
extensions = context_extensions.each_with_object({}) do |key, h|
|
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,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,121 +0,0 @@
|
||||||
import 'packs/public-path';
|
|
||||||
|
|
||||||
import * as WebAuthnJSON from '@github/webauthn-json';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import ready from '../mastodon/ready';
|
|
||||||
import 'regenerator-runtime/runtime';
|
|
||||||
|
|
||||||
function getCSRFToken() {
|
|
||||||
var CSRFSelector = document.querySelector('meta[name="csrf-token"]');
|
|
||||||
if (CSRFSelector) {
|
|
||||||
return CSRFSelector.getAttribute('content');
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideFlashMessages() {
|
|
||||||
Array.from(document.getElementsByClassName('flash-message')).forEach(function(flashMessage) {
|
|
||||||
flashMessage.classList.add('hidden');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function callback(url, body) {
|
|
||||||
axios.post(url, JSON.stringify(body), {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'X-CSRF-Token': getCSRFToken(),
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
}).then(function(response) {
|
|
||||||
window.location.replace(response.data.redirect_path);
|
|
||||||
}).catch(function(error) {
|
|
||||||
if (error.response.status === 422) {
|
|
||||||
const errorMessage = document.getElementById('security-key-error-message');
|
|
||||||
errorMessage.classList.remove('hidden');
|
|
||||||
console.error(error.response.data.error);
|
|
||||||
} else {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ready(() => {
|
|
||||||
if (!WebAuthnJSON.supported()) {
|
|
||||||
const unsupported_browser_message = document.getElementById('unsupported-browser-message');
|
|
||||||
if (unsupported_browser_message) {
|
|
||||||
unsupported_browser_message.classList.remove('hidden');
|
|
||||||
document.querySelector('.btn.js-webauthn').disabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const webAuthnCredentialRegistrationForm = document.getElementById('new_webauthn_credential');
|
|
||||||
if (webAuthnCredentialRegistrationForm) {
|
|
||||||
webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
var nickname = event.target.querySelector('input[name="new_webauthn_credential[nickname]"]');
|
|
||||||
if (nickname.value) {
|
|
||||||
axios.get('/settings/security_keys/options')
|
|
||||||
.then((response) => {
|
|
||||||
const credentialOptions = response.data;
|
|
||||||
|
|
||||||
WebAuthnJSON.create({ 'publicKey': credentialOptions }).then((credential) => {
|
|
||||||
var params = { 'credential': credential, 'nickname': nickname.value };
|
|
||||||
callback('/settings/security_keys', params);
|
|
||||||
}).catch((error) => {
|
|
||||||
const errorMessage = document.getElementById('security-key-error-message');
|
|
||||||
errorMessage.classList.remove('hidden');
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error.response.data.error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
nickname.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const webAuthnCredentialAuthenticationForm = document.getElementById('webauthn-form');
|
|
||||||
if (webAuthnCredentialAuthenticationForm) {
|
|
||||||
webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
axios.get('sessions/security_key_options')
|
|
||||||
.then((response) => {
|
|
||||||
const credentialOptions = response.data;
|
|
||||||
|
|
||||||
WebAuthnJSON.get({ 'publicKey': credentialOptions }).then((credential) => {
|
|
||||||
var params = { 'user': { 'credential': credential } };
|
|
||||||
callback('sign_in', params);
|
|
||||||
}).catch((error) => {
|
|
||||||
const errorMessage = document.getElementById('security-key-error-message');
|
|
||||||
errorMessage.classList.remove('hidden');
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error.response.data.error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const otpAuthenticationForm = document.getElementById('otp-authentication-form');
|
|
||||||
|
|
||||||
const linkToOtp = document.getElementById('link-to-otp');
|
|
||||||
linkToOtp.addEventListener('click', () => {
|
|
||||||
webAuthnCredentialAuthenticationForm.classList.add('hidden');
|
|
||||||
otpAuthenticationForm.classList.remove('hidden');
|
|
||||||
hideFlashMessages();
|
|
||||||
});
|
|
||||||
|
|
||||||
const linkToWebAuthn = document.getElementById('link-to-webauthn');
|
|
||||||
linkToWebAuthn.addEventListener('click', () => {
|
|
||||||
otpAuthenticationForm.classList.add('hidden');
|
|
||||||
webAuthnCredentialAuthenticationForm.classList.remove('hidden');
|
|
||||||
hideFlashMessages();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,6 +1,5 @@
|
||||||
// This file will be loaded on admin pages, regardless of theme.
|
import './public-path';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
import 'packs/public-path';
|
|
||||||
|
|
||||||
import Rails from '@rails/ujs';
|
import Rails from '@rails/ujs';
|
||||||
|
|
||||||
|
@ -261,6 +260,31 @@ Rails.delegate(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function mountReactComponent(element: Element) {
|
||||||
|
const componentName = element.getAttribute('data-admin-component');
|
||||||
|
const stringProps = element.getAttribute('data-props');
|
||||||
|
|
||||||
|
if (!stringProps) return;
|
||||||
|
|
||||||
|
const componentProps = JSON.parse(stringProps) as object;
|
||||||
|
|
||||||
|
const { default: AdminComponent } = await import(
|
||||||
|
'@/mastodon/containers/admin_component'
|
||||||
|
);
|
||||||
|
|
||||||
|
const { default: Component } = (await import(
|
||||||
|
`@/mastodon/components/admin/${componentName}`
|
||||||
|
)) as { default: React.ComponentType };
|
||||||
|
|
||||||
|
const root = createRoot(element);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<AdminComponent>
|
||||||
|
<Component {...componentProps} />
|
||||||
|
</AdminComponent>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ready(() => {
|
ready(() => {
|
||||||
const domainBlockSeveritySelect = document.querySelector<HTMLSelectElement>(
|
const domainBlockSeveritySelect = document.querySelector<HTMLSelectElement>(
|
||||||
'select#domain_block_severity',
|
'select#domain_block_severity',
|
||||||
|
@ -335,6 +359,10 @@ ready(() => {
|
||||||
if (announcementStartsAt) {
|
if (announcementStartsAt) {
|
||||||
setAnnouncementEndsAttributes(announcementStartsAt);
|
setAnnouncementEndsAttributes(announcementStartsAt);
|
||||||
}
|
}
|
||||||
}).catch((reason) => {
|
|
||||||
|
document.querySelectorAll('[data-admin-component]').forEach((element) => {
|
||||||
|
void mountReactComponent(element);
|
||||||
|
});
|
||||||
|
}).catch((reason: unknown) => {
|
||||||
throw reason;
|
throw reason;
|
||||||
});
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
import './public-path';
|
import './public-path';
|
||||||
import main from "mastodon/main";
|
import main from 'mastodon/main';
|
||||||
|
|
||||||
import { start } from '../mastodon/common';
|
import { start } from '../mastodon/common';
|
||||||
import { loadLocale } from '../mastodon/locales';
|
import { loadLocale } from '../mastodon/locales';
|
||||||
|
@ -10,6 +10,6 @@ start();
|
||||||
loadPolyfills()
|
loadPolyfills()
|
||||||
.then(loadLocale)
|
.then(loadLocale)
|
||||||
.then(main)
|
.then(main)
|
||||||
.catch(e => {
|
.catch((e: unknown) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
5
app/javascript/entrypoints/common.js
Normal file
5
app/javascript/entrypoints/common.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/* 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 './public-path';
|
||||||
|
import 'font-awesome/css/font-awesome.css';
|
|
@ -2,7 +2,9 @@ import './public-path';
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
ready(() => {
|
ready(() => {
|
||||||
const image = document.querySelector('img');
|
const image = document.querySelector<HTMLImageElement>('img');
|
||||||
|
|
||||||
|
if (!image) return;
|
||||||
|
|
||||||
image.addEventListener('mouseenter', () => {
|
image.addEventListener('mouseenter', () => {
|
||||||
image.src = '/oops.gif';
|
image.src = '/oops.gif';
|
||||||
|
@ -11,4 +13,6 @@ ready(() => {
|
||||||
image.addEventListener('mouseleave', () => {
|
image.addEventListener('mouseleave', () => {
|
||||||
image.src = '/oops.png';
|
image.src = '/oops.png';
|
||||||
});
|
});
|
||||||
|
}).catch((e: unknown) => {
|
||||||
|
console.error(e);
|
||||||
});
|
});
|
|
@ -2,7 +2,7 @@
|
||||||
// to share the same assets regardless of instance configuration.
|
// to share the same assets regardless of instance configuration.
|
||||||
// See https://webpack.js.org/guides/public-path/#on-the-fly
|
// See https://webpack.js.org/guides/public-path/#on-the-fly
|
||||||
|
|
||||||
function removeOuterSlashes(string) {
|
function removeOuterSlashes(string: string) {
|
||||||
return string.replace(/^\/*/, '').replace(/\/*$/, '');
|
return string.replace(/^\/*/, '').replace(/\/*$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,9 @@ function formatPublicPath(host = '', path = '') {
|
||||||
return `${formattedHost}/${formattedPath}/`;
|
return `${formattedHost}/${formattedPath}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cdnHost = document.querySelector('meta[name=cdn-host]');
|
const cdnHost = document.querySelector<HTMLMetaElement>('meta[name=cdn-host]');
|
||||||
|
|
||||||
// eslint-disable-next-line no-undef
|
__webpack_public_path__ = formatPublicPath(
|
||||||
__webpack_public_path__ = formatPublicPath(cdnHost ? cdnHost.content : '', process.env.PUBLIC_OUTPUT_PATH);
|
cdnHost ? cdnHost.content : '',
|
||||||
|
process.env.PUBLIC_OUTPUT_PATH,
|
||||||
|
);
|
|
@ -37,6 +37,43 @@ const messages = defineMessages({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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: unknown) => {
|
||||||
|
console.error('Error in setHeightMessage postMessage', e);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function loaded() {
|
function loaded() {
|
||||||
const { messages: localeData } = getLocale();
|
const { messages: localeData } = getLocale();
|
||||||
|
|
||||||
|
@ -169,7 +206,7 @@ function loaded() {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error: unknown) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -288,6 +325,72 @@ function loaded() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
const sidebar = document.querySelector<HTMLUListElement>('.sidebar ul');
|
const sidebar = document.querySelector<HTMLUListElement>('.sidebar ul');
|
||||||
const toggleButton = document.querySelector<HTMLAnchorElement>(
|
const toggleButton = document.querySelector<HTMLAnchorElement>(
|
||||||
|
@ -345,7 +448,7 @@ Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
ready(loaded).catch((error) => {
|
ready(loaded).catch((error: unknown) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -354,6 +457,6 @@ loadPolyfills()
|
||||||
.then(loadLocale)
|
.then(loadLocale)
|
||||||
.then(main)
|
.then(main)
|
||||||
.then(loadKeyboardExtensions)
|
.then(loadKeyboardExtensions)
|
||||||
.catch((error) => {
|
.catch((error: unknown) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
|
@ -8,7 +8,7 @@ and performs no other task.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'packs/public-path';
|
import './public-path';
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
|
@ -2,7 +2,7 @@ import './public-path';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { start } from '../mastodon/common';
|
import { start } from '../mastodon/common';
|
||||||
import ComposeContainer from '../mastodon/containers/compose_container';
|
import ComposeContainer from '../mastodon/containers/compose_container';
|
||||||
import { loadPolyfills } from '../mastodon/polyfills';
|
import { loadPolyfills } from '../mastodon/polyfills';
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ function loaded() {
|
||||||
|
|
||||||
if (!attr) return;
|
if (!attr) return;
|
||||||
|
|
||||||
const props = JSON.parse(attr);
|
const props = JSON.parse(attr) as object;
|
||||||
const root = createRoot(mountNode);
|
const root = createRoot(mountNode);
|
||||||
|
|
||||||
root.render(<ComposeContainer {...props} />);
|
root.render(<ComposeContainer {...props} />);
|
||||||
|
@ -24,9 +24,13 @@ function loaded() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
ready(loaded);
|
ready(loaded).catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPolyfills().then(main).catch(error => {
|
loadPolyfills()
|
||||||
console.error(error);
|
.then(main)
|
||||||
});
|
.catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
48
app/javascript/entrypoints/sign_up.ts
Normal file
48
app/javascript/entrypoints/sign_up.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import './public-path';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
|
async function checkConfirmation() {
|
||||||
|
const response = await axios.get('/api/v1/emails/check_confirmation');
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
window.location.href = '/start';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
setInterval(() => {
|
||||||
|
void checkConfirmation();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLButtonElement>('button.timer-button')
|
||||||
|
.forEach((button) => {
|
||||||
|
let counter = 30;
|
||||||
|
|
||||||
|
const container = document.createElement('span');
|
||||||
|
|
||||||
|
const updateCounter = () => {
|
||||||
|
container.innerText = ` (${counter})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCounter();
|
||||||
|
|
||||||
|
const countdown = setInterval(() => {
|
||||||
|
counter--;
|
||||||
|
|
||||||
|
if (counter === 0) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.removeChild(container);
|
||||||
|
clearInterval(countdown);
|
||||||
|
} else {
|
||||||
|
updateCounter();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
button.appendChild(container);
|
||||||
|
});
|
||||||
|
}).catch((e: unknown) => {
|
||||||
|
throw e;
|
||||||
|
});
|
197
app/javascript/entrypoints/two_factor_authentication.ts
Normal file
197
app/javascript/entrypoints/two_factor_authentication.ts
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
import * as WebAuthnJSON from '@github/webauthn-json';
|
||||||
|
import axios, { AxiosError } from 'axios';
|
||||||
|
|
||||||
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
|
import 'regenerator-runtime/runtime';
|
||||||
|
|
||||||
|
type PublicKeyCredentialCreationOptionsJSON =
|
||||||
|
WebAuthnJSON.CredentialCreationOptionsJSON['publicKey'];
|
||||||
|
|
||||||
|
function exceptionHasAxiosError(
|
||||||
|
error: unknown,
|
||||||
|
): error is AxiosError<{ error: unknown }> {
|
||||||
|
return (
|
||||||
|
error instanceof AxiosError &&
|
||||||
|
typeof error.response?.data === 'object' &&
|
||||||
|
'error' in error.response.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logAxiosResponseError(error: unknown) {
|
||||||
|
if (exceptionHasAxiosError(error)) console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCSRFToken() {
|
||||||
|
return document
|
||||||
|
.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')
|
||||||
|
?.getAttribute('content');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideFlashMessages() {
|
||||||
|
document.querySelectorAll('.flash-message').forEach((flashMessage) => {
|
||||||
|
flashMessage.classList.add('hidden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callback(
|
||||||
|
url: string,
|
||||||
|
body:
|
||||||
|
| {
|
||||||
|
credential: WebAuthnJSON.PublicKeyCredentialWithAttestationJSON;
|
||||||
|
nickname: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
user: { credential: WebAuthnJSON.PublicKeyCredentialWithAssertionJSON };
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<{ redirect_path: string }>(
|
||||||
|
url,
|
||||||
|
JSON.stringify(body),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
'X-CSRF-Token': getCSRFToken(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
window.location.replace(response.data.redirect_path);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError && error.response?.status === 422) {
|
||||||
|
const errorMessage = document.getElementById(
|
||||||
|
'security-key-error-message',
|
||||||
|
);
|
||||||
|
errorMessage?.classList.remove('hidden');
|
||||||
|
|
||||||
|
logAxiosResponseError(error);
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWebauthnCredentialRegistration(nickname: string) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<PublicKeyCredentialCreationOptionsJSON>(
|
||||||
|
'/settings/security_keys/options',
|
||||||
|
);
|
||||||
|
|
||||||
|
const credentialOptions = response.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const credential = await WebAuthnJSON.create({
|
||||||
|
publicKey: credentialOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
credential: credential,
|
||||||
|
nickname: nickname,
|
||||||
|
};
|
||||||
|
|
||||||
|
await callback('/settings/security_keys', params);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = document.getElementById(
|
||||||
|
'security-key-error-message',
|
||||||
|
);
|
||||||
|
errorMessage?.classList.remove('hidden');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosResponseError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWebauthnCredentialAuthentication() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<PublicKeyCredentialCreationOptionsJSON>(
|
||||||
|
'sessions/security_key_options',
|
||||||
|
);
|
||||||
|
|
||||||
|
const credentialOptions = response.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const credential = await WebAuthnJSON.get({
|
||||||
|
publicKey: credentialOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = { user: { credential: credential } };
|
||||||
|
void callback('sign_in', params);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = document.getElementById(
|
||||||
|
'security-key-error-message',
|
||||||
|
);
|
||||||
|
errorMessage?.classList.remove('hidden');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logAxiosResponseError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
if (!WebAuthnJSON.supported()) {
|
||||||
|
const unsupported_browser_message = document.getElementById(
|
||||||
|
'unsupported-browser-message',
|
||||||
|
);
|
||||||
|
if (unsupported_browser_message) {
|
||||||
|
unsupported_browser_message.classList.remove('hidden');
|
||||||
|
const button = document.querySelector<HTMLButtonElement>(
|
||||||
|
'button.btn.js-webauthn',
|
||||||
|
);
|
||||||
|
if (button) button.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const webAuthnCredentialRegistrationForm =
|
||||||
|
document.querySelector<HTMLFormElement>('form#new_webauthn_credential');
|
||||||
|
if (webAuthnCredentialRegistrationForm) {
|
||||||
|
webAuthnCredentialRegistrationForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!(event.target instanceof HTMLFormElement)) return;
|
||||||
|
|
||||||
|
const nickname = event.target.querySelector<HTMLInputElement>(
|
||||||
|
'input[name="new_webauthn_credential[nickname]"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nickname?.value) {
|
||||||
|
void handleWebauthnCredentialRegistration(nickname.value);
|
||||||
|
} else {
|
||||||
|
nickname?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const webAuthnCredentialAuthenticationForm =
|
||||||
|
document.getElementById('webauthn-form');
|
||||||
|
if (webAuthnCredentialAuthenticationForm) {
|
||||||
|
webAuthnCredentialAuthenticationForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleWebauthnCredentialAuthentication();
|
||||||
|
});
|
||||||
|
|
||||||
|
const otpAuthenticationForm = document.getElementById(
|
||||||
|
'otp-authentication-form',
|
||||||
|
);
|
||||||
|
|
||||||
|
const linkToOtp = document.getElementById('link-to-otp');
|
||||||
|
|
||||||
|
linkToOtp?.addEventListener('click', () => {
|
||||||
|
webAuthnCredentialAuthenticationForm.classList.add('hidden');
|
||||||
|
otpAuthenticationForm?.classList.remove('hidden');
|
||||||
|
hideFlashMessages();
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkToWebAuthn = document.getElementById('link-to-webauthn');
|
||||||
|
linkToWebAuthn?.addEventListener('click', () => {
|
||||||
|
otpAuthenticationForm?.classList.add('hidden');
|
||||||
|
webAuthnCredentialAuthenticationForm.classList.remove('hidden');
|
||||||
|
hideFlashMessages();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((e: unknown) => {
|
||||||
|
throw e;
|
||||||
|
});
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
147
app/javascript/flavours/glitch/actions/markers.ts
Normal file
147
app/javascript/flavours/glitch/actions/markers.ts
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import type { MarkerJSON } from 'flavours/glitch/api_types/markers';
|
||||||
|
import type { AppDispatch, 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 getLastNotificationId(state: RootState): string | undefined {
|
||||||
|
// @ts-expect-error state.notifications is not yet typed
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
||||||
|
return state.getIn(['notifications', 'lastReadId']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildPostMarkersParams = (state: RootState) => {
|
||||||
|
const params = {} as { home?: MarkerParam; notifications?: MarkerParam };
|
||||||
|
|
||||||
|
const lastNotificationId = getLastNotificationId(state);
|
||||||
|
|
||||||
|
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: AppDispatch) => {
|
||||||
|
void 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);
|
void ready(setCSRFHeader);
|
||||||
|
|
||||||
|
export const authorizationTokenFromState = (getState?: GetState) => {
|
||||||
|
return (
|
||||||
|
getState && (getState().meta.get('access_token', '') as string | false)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const authorizationHeaderFromState = (getState?: GetState) => {
|
const authorizationHeaderFromState = (getState?: GetState) => {
|
||||||
const accessToken =
|
const accessToken = authorizationTokenFromState(getState);
|
||||||
getState && (getState().meta.get('access_token', '') as string);
|
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return {};
|
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,16 +1,18 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
import { EmptyAccount } from 'flavours/glitch/components/empty_account';
|
import { EmptyAccount } from 'flavours/glitch/components/empty_account';
|
||||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||||
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
import { VerifiedBadge } from 'flavours/glitch/components/verified_badge';
|
||||||
|
|
||||||
|
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
||||||
import { me } from '../initial_state';
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
import { Avatar } from './avatar';
|
||||||
|
@ -30,153 +32,153 @@ const messages = defineMessages({
|
||||||
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
|
unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' },
|
||||||
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
|
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
|
||||||
block: { id: 'account.block_short', defaultMessage: 'Block' },
|
block: { id: 'account.block_short', defaultMessage: 'Block' },
|
||||||
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class Account extends ImmutablePureComponent {
|
const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, minimal, defaultAction, withBio }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
static propTypes = {
|
const handleFollow = useCallback(() => {
|
||||||
size: PropTypes.number,
|
onFollow(account);
|
||||||
account: ImmutablePropTypes.record,
|
}, [onFollow, account]);
|
||||||
onFollow: PropTypes.func,
|
|
||||||
onBlock: PropTypes.func,
|
|
||||||
onMute: PropTypes.func,
|
|
||||||
onMuteNotifications: PropTypes.func,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
hidden: PropTypes.bool,
|
|
||||||
minimal: PropTypes.bool,
|
|
||||||
defaultAction: PropTypes.string,
|
|
||||||
withBio: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
const handleBlock = useCallback(() => {
|
||||||
size: 46,
|
onBlock(account);
|
||||||
};
|
}, [onBlock, account]);
|
||||||
|
|
||||||
handleFollow = () => {
|
const handleMute = useCallback(() => {
|
||||||
this.props.onFollow(this.props.account);
|
onMute(account);
|
||||||
};
|
}, [onMute, account]);
|
||||||
|
|
||||||
handleBlock = () => {
|
const handleMuteNotifications = useCallback(() => {
|
||||||
this.props.onBlock(this.props.account);
|
onMuteNotifications(account, true);
|
||||||
};
|
}, [onMuteNotifications, account]);
|
||||||
|
|
||||||
handleMute = () => {
|
const handleUnmuteNotifications = useCallback(() => {
|
||||||
this.props.onMute(this.props.account);
|
onMuteNotifications(account, false);
|
||||||
};
|
}, [onMuteNotifications, account]);
|
||||||
|
|
||||||
handleMuteNotifications = () => {
|
if (!account) {
|
||||||
this.props.onMuteNotifications(this.props.account, true);
|
return <EmptyAccount size={size} minimal={minimal} />;
|
||||||
};
|
}
|
||||||
|
|
||||||
handleUnmuteNotifications = () => {
|
|
||||||
this.props.onMuteNotifications(this.props.account, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account, intl, hidden, withBio, defaultAction, size, minimal } = this.props;
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return <EmptyAccount size={size} minimal={minimal} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hidden) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{account.get('display_name')}
|
|
||||||
{account.get('username')}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let buttons;
|
|
||||||
|
|
||||||
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
|
||||||
const following = account.getIn(['relationship', 'following']);
|
|
||||||
const requested = account.getIn(['relationship', 'requested']);
|
|
||||||
const blocking = account.getIn(['relationship', 'blocking']);
|
|
||||||
const muting = account.getIn(['relationship', 'muting']);
|
|
||||||
|
|
||||||
if (requested) {
|
|
||||||
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
|
|
||||||
} else if (blocking) {
|
|
||||||
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
|
||||||
} else if (muting) {
|
|
||||||
let hidingNotificationsButton;
|
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'muting_notifications'])) {
|
|
||||||
hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
|
|
||||||
} else {
|
|
||||||
hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons = (
|
|
||||||
<>
|
|
||||||
<Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
|
|
||||||
{hidingNotificationsButton}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (defaultAction === 'mute') {
|
|
||||||
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
|
|
||||||
} else if (defaultAction === 'block') {
|
|
||||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
|
|
||||||
} else if (!account.get('suspended') && !account.get('moved') || following) {
|
|
||||||
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let muteTimeRemaining;
|
|
||||||
|
|
||||||
if (account.get('mute_expires_at')) {
|
|
||||||
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let verification;
|
|
||||||
|
|
||||||
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
|
|
||||||
|
|
||||||
if (firstVerifiedField) {
|
|
||||||
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('account', { 'account--minimal': minimal })}>
|
<>
|
||||||
<div className='account__wrapper'>
|
{account.get('display_name')}
|
||||||
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
{account.get('username')}
|
||||||
<div className='account__avatar-wrapper'>
|
</>
|
||||||
<Avatar account={account} size={size} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='account__contents'>
|
|
||||||
<DisplayName account={account} />
|
|
||||||
{!minimal && (
|
|
||||||
<div className='account__details'>
|
|
||||||
{account.get('followers_count') !== -1 && (
|
|
||||||
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} />
|
|
||||||
)} {verification} {muteTimeRemaining}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Permalink>
|
|
||||||
|
|
||||||
{!minimal && (
|
|
||||||
<div className='account__relationship'>
|
|
||||||
{buttons}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{withBio && (account.get('note').length > 0 ? (
|
|
||||||
<div
|
|
||||||
className='account__note translate'
|
|
||||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
let buttons;
|
||||||
|
|
||||||
export default injectIntl(Account);
|
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||||
|
const following = account.getIn(['relationship', 'following']);
|
||||||
|
const requested = account.getIn(['relationship', 'requested']);
|
||||||
|
const blocking = account.getIn(['relationship', 'blocking']);
|
||||||
|
const muting = account.getIn(['relationship', 'muting']);
|
||||||
|
|
||||||
|
if (requested) {
|
||||||
|
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={handleFollow} />;
|
||||||
|
} else if (blocking) {
|
||||||
|
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={handleBlock} />;
|
||||||
|
} else if (muting) {
|
||||||
|
let menu;
|
||||||
|
|
||||||
|
if (account.getIn(['relationship', 'muting_notifications'])) {
|
||||||
|
menu = [{ text: intl.formatMessage(messages.unmute_notifications), action: handleUnmuteNotifications }];
|
||||||
|
} else {
|
||||||
|
menu = [{ text: intl.formatMessage(messages.mute_notifications), action: handleMuteNotifications }];
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons = (
|
||||||
|
<>
|
||||||
|
<DropdownMenuContainer
|
||||||
|
items={menu}
|
||||||
|
icon='ellipsis-h'
|
||||||
|
iconComponent={MoreHorizIcon}
|
||||||
|
direction='right'
|
||||||
|
title={intl.formatMessage(messages.more)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button text={intl.formatMessage(messages.unmute)} onClick={handleMute} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (defaultAction === 'mute') {
|
||||||
|
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={handleMute} />;
|
||||||
|
} else if (defaultAction === 'block') {
|
||||||
|
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
|
||||||
|
} else if (!account.get('suspended') && !account.get('moved') || following) {
|
||||||
|
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={handleFollow} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let muteTimeRemaining;
|
||||||
|
|
||||||
|
if (account.get('mute_expires_at')) {
|
||||||
|
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let verification;
|
||||||
|
|
||||||
|
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
|
||||||
|
|
||||||
|
if (firstVerifiedField) {
|
||||||
|
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('account', { 'account--minimal': minimal })}>
|
||||||
|
<div className='account__wrapper'>
|
||||||
|
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}>
|
||||||
|
<div className='account__avatar-wrapper'>
|
||||||
|
<Avatar account={account} size={size} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='account__contents'>
|
||||||
|
<DisplayName account={account} />
|
||||||
|
{!minimal && (
|
||||||
|
<div className='account__details'>
|
||||||
|
{account.get('followers_count') !== -1 && (
|
||||||
|
<ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} />
|
||||||
|
)} {verification} {muteTimeRemaining}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Permalink>
|
||||||
|
|
||||||
|
{!minimal && (
|
||||||
|
<div className='account__relationship'>
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{withBio && (account.get('note').length > 0 ? (
|
||||||
|
<div
|
||||||
|
className='account__note translate'
|
||||||
|
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Account.propTypes = {
|
||||||
|
size: PropTypes.number,
|
||||||
|
account: ImmutablePropTypes.record,
|
||||||
|
onFollow: PropTypes.func,
|
||||||
|
onBlock: PropTypes.func,
|
||||||
|
onMute: PropTypes.func,
|
||||||
|
onMuteNotifications: PropTypes.func,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
|
minimal: PropTypes.bool,
|
||||||
|
defaultAction: PropTypes.string,
|
||||||
|
withBio: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Account;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import PersonIcon from '@/material-icons/400-24px/person.svg?react';
|
||||||
import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react';
|
import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react';
|
||||||
|
|
||||||
|
|
||||||
export const Badge = ({ icon, label, domain, roleId }) => (
|
export const Badge = ({ icon = <PersonIcon />, label, domain, roleId }) => (
|
||||||
<div className='account-role' data-account-role-id={roleId}>
|
<div className='account-role' data-account-role-id={roleId}>
|
||||||
{icon}
|
{icon}
|
||||||
{label}
|
{label}
|
||||||
|
@ -22,10 +22,6 @@ Badge.propTypes = {
|
||||||
roleId: PropTypes.string
|
roleId: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
Badge.defaultProps = {
|
|
||||||
icon: <PersonIcon />,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GroupBadge = () => (
|
export const GroupBadge = () => (
|
||||||
<Badge icon={<GroupsIcon />} label={<FormattedMessage id='account.badges.group' defaultMessage='Group' />} />
|
<Badge icon={<GroupsIcon />} label={<FormattedMessage id='account.badges.group' defaultMessage='Group' />} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface BaseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface BaseProps
|
||||||
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
text?: JSX.Element;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PropsWithChildren extends BaseProps {
|
interface PropsChildren extends PropsWithChildren<BaseProps> {
|
||||||
text?: never;
|
text?: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PropsWithText extends BaseProps {
|
interface PropsWithText extends BaseProps {
|
||||||
text: JSX.Element;
|
text: JSX.Element | string;
|
||||||
children: never;
|
children?: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = PropsWithText | PropsWithChildren;
|
type Props = PropsWithText | PropsChildren;
|
||||||
|
|
||||||
export const Button: React.FC<Props> = ({
|
export const Button: React.FC<Props> = ({
|
||||||
text,
|
|
||||||
type = 'button',
|
type = 'button',
|
||||||
onClick,
|
onClick,
|
||||||
disabled,
|
disabled,
|
||||||
|
@ -28,6 +28,7 @@ export const Button: React.FC<Props> = ({
|
||||||
secondary,
|
secondary,
|
||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
|
text,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
|
|
@ -24,7 +24,7 @@ export type StatusLike = Record<{
|
||||||
|
|
||||||
function normalizeHashtag(hashtag: string) {
|
function normalizeHashtag(hashtag: string) {
|
||||||
return (
|
return (
|
||||||
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
|
!!hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
|
||||||
).normalize('NFKC');
|
).normalize('NFKC');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue