Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `.github/dependabot.yml`:
  Updated upstream, removed in glitch-soc to disable noise.
  Kept removed.
- `CODE_OF_CONDUCT.md`:
  Upstream updated to a new version of the covenant, but I have not read it
  yet, so kept unchanged.
- `Gemfile.lock`:
  Not a real conflict, one upstream dependency updated textually too close to
  the glitch-soc only `hcaptcha` dependency.
  Applied upstream changes.
- `app/controllers/admin/base_controller.rb`:
  Minor conflict due to glitch-soc's theming system.
  Applied upstream changes.
- `app/controllers/application_controller.rb`:
  Minor conflict due to glitch-soc's theming system.
  Applied upstream changes.
- `app/controllers/disputes/base_controller.rb`:
  Minor conflict due to glitch-soc's theming system.
  Applied upstream changes.
- `app/controllers/relationships_controller.rb`:
  Minor conflict due to glitch-soc's theming system.
  Applied upstream changes.
- `app/controllers/statuses_cleanup_controller.rb`:
  Minor conflict due to glitch-soc's theming system.
  Applied upstream changes.
- `app/helpers/application_helper.rb`:
  Minor conflict due to glitch-soc's theming system.
  Applied upstream changes.
- `app/javascript/mastodon/features/compose/components/compose_form.jsx`:
  Upstream added a highlight animation for onboarding, while we changed the
  max character limit.
  Applied our local changes on top of upstream's new version.
- `app/views/layouts/application.html.haml`:
  Minor conflict due to glitch-soc's theming system.
  Applied upstream changes.
- `stylelint.config.js`:
  Upstream added ignore paths, glitch-soc had extra ignore paths.
  Added the same paths as upstream.
th-downstream
Claire 2 years ago
commit 12b935fadf

@ -27,6 +27,7 @@ module.exports = {
'import', 'import',
'promise', 'promise',
'@typescript-eslint', '@typescript-eslint',
'formatjs',
], ],
parserOptions: { parserOptions: {
@ -71,7 +72,7 @@ module.exports = {
'comma-style': ['warn', 'last'], 'comma-style': ['warn', 'last'],
'consistent-return': 'error', 'consistent-return': 'error',
'dot-notation': 'error', 'dot-notation': 'error',
eqeqeq: 'error', eqeqeq: ['error', 'always', { 'null': 'ignore' }],
indent: ['warn', 2], indent: ['warn', 2],
'jsx-quotes': ['error', 'prefer-single'], 'jsx-quotes': ['error', 'prefer-single'],
'no-case-declarations': 'off', 'no-case-declarations': 'off',
@ -218,6 +219,25 @@ module.exports = {
'promise/no-callback-in-promise': 'off', 'promise/no-callback-in-promise': 'off',
'promise/no-nesting': 'off', 'promise/no-nesting': 'off',
'promise/no-promise-in-callback': 'off', 'promise/no-promise-in-callback': 'off',
'formatjs/blocklist-elements': 'error',
'formatjs/enforce-default-message': ['error', 'literal'],
'formatjs/enforce-description': 'off', // description values not currently used
'formatjs/enforce-id': 'off', // Explicit IDs are used in the project
'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx
'formatjs/enforce-plural-rules': 'error',
'formatjs/no-camel-case': 'off', // disabledAccount is only non-conforming
'formatjs/no-complex-selectors': 'error',
'formatjs/no-emoji': 'error',
'formatjs/no-id': 'off', // IDs are used for translation keys
'formatjs/no-invalid-icu': 'error',
'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings
'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx
'formatjs/no-multiple-whitespaces': 'error',
'formatjs/no-offset': 'error',
'formatjs/no-useless-message': 'error',
'formatjs/prefer-formatted-message': 'error',
'formatjs/prefer-pound-in-plural': 'error',
}, },
overrides: [ overrides: [

@ -0,0 +1,54 @@
name: Build nightly container image
on:
workflow_dispatch:
schedule:
- cron: '0 2 * * *' # run at 2 AM UTC
permissions:
contents: read
packages: write
jobs:
build-nightly-image:
runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: hadolint/hadolint-action@v3.1.0
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- name: Log in to the Github Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v4
id: meta
with:
images: |
ghcr.io/mastodon/mastodon
flavor: |
latest=auto
tags: |
type=raw,value=nightly
type=schedule,pattern=nightly-{{date 'YYYY-MM-DD' tz='Etc/UTC'}}
labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes
- uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
provenance: false
builder: ${{ steps.buildx.outputs.name }}
push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

@ -104,7 +104,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
ruby-version: ruby-version:
- '2.7'
- '3.0' - '3.0'
- '3.1' - '3.1'
- '.ruby-version' - '.ruby-version'
@ -136,10 +135,6 @@ jobs:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
bundler-cache: true bundler-cache: true
- name: Update system gems
if: matrix.ruby-version == '2.7'
run: gem update --system
- name: Load database schema - name: Load database schema
run: './bin/rails db:create db:schema:load db:seed' run: './bin/rails db:create db:schema:load db:seed'

@ -13,7 +13,7 @@ require:
- rubocop-capybara - rubocop-capybara
AllCops: AllCops:
TargetRubyVersion: 2.7 # Set to minimum supported version of CI TargetRubyVersion: 3.0 # Set to minimum supported version of CI
DisplayCopNames: true DisplayCopNames: true
DisplayStyleGuide: true DisplayStyleGuide: true
ExtraDetails: true ExtraDetails: true

@ -1,6 +1,6 @@
# 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.48.1. # using RuboCop version 1.50.2.
# 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
@ -132,7 +132,6 @@ Lint/DuplicateBranch:
Lint/EmptyBlock: Lint/EmptyBlock:
Exclude: Exclude:
- 'spec/controllers/api/v2/search_controller_spec.rb' - 'spec/controllers/api/v2/search_controller_spec.rb'
- 'spec/controllers/application_controller_spec.rb'
- 'spec/fabricators/access_token_fabricator.rb' - 'spec/fabricators/access_token_fabricator.rb'
- 'spec/fabricators/conversation_fabricator.rb' - 'spec/fabricators/conversation_fabricator.rb'
- 'spec/fabricators/system_key_fabricator.rb' - 'spec/fabricators/system_key_fabricator.rb'
@ -174,11 +173,6 @@ Lint/EmptyClass:
Exclude: Exclude:
- 'spec/controllers/api/base_controller_spec.rb' - 'spec/controllers/api/base_controller_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/NonDeterministicRequireOrder:
Exclude:
- 'spec/rails_helper.rb'
Lint/NonLocalExitFromIterator: Lint/NonLocalExitFromIterator:
Exclude: Exclude:
- 'app/helpers/jsonld_helper.rb' - 'app/helpers/jsonld_helper.rb'
@ -251,7 +245,6 @@ Metrics/ModuleLength:
- 'app/controllers/concerns/signature_verification.rb' - 'app/controllers/concerns/signature_verification.rb'
- 'app/helpers/application_helper.rb' - 'app/helpers/application_helper.rb'
- 'app/helpers/jsonld_helper.rb' - 'app/helpers/jsonld_helper.rb'
- 'app/helpers/statuses_helper.rb'
- 'app/models/concerns/account_interactions.rb' - 'app/models/concerns/account_interactions.rb'
- 'app/models/concerns/has_user_settings.rb' - 'app/models/concerns/has_user_settings.rb'
@ -370,6 +363,7 @@ Performance/MethodObjectAsBlock:
- 'spec/models/export_spec.rb' - 'spec/models/export_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowRegexpMatch.
Performance/RedundantEqualityComparisonBlock: Performance/RedundantEqualityComparisonBlock:
Exclude: Exclude:
- 'spec/requests/link_headers_spec.rb' - 'spec/requests/link_headers_spec.rb'
@ -699,7 +693,6 @@ RSpec/HookArgument:
RSpec/InstanceVariable: RSpec/InstanceVariable:
Exclude: Exclude:
- 'spec/controllers/api/v1/streaming_controller_spec.rb' - 'spec/controllers/api/v1/streaming_controller_spec.rb'
- 'spec/controllers/application_controller_spec.rb'
- 'spec/controllers/auth/confirmations_controller_spec.rb' - 'spec/controllers/auth/confirmations_controller_spec.rb'
- 'spec/controllers/auth/passwords_controller_spec.rb' - 'spec/controllers/auth/passwords_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb' - 'spec/controllers/auth/sessions_controller_spec.rb'
@ -753,7 +746,6 @@ RSpec/LetSetup:
- 'spec/controllers/following_accounts_controller_spec.rb' - 'spec/controllers/following_accounts_controller_spec.rb'
- 'spec/controllers/oauth/authorized_applications_controller_spec.rb' - 'spec/controllers/oauth/authorized_applications_controller_spec.rb'
- 'spec/controllers/oauth/tokens_controller_spec.rb' - 'spec/controllers/oauth/tokens_controller_spec.rb'
- 'spec/controllers/tags_controller_spec.rb'
- 'spec/lib/activitypub/activity/delete_spec.rb' - 'spec/lib/activitypub/activity/delete_spec.rb'
- 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
@ -780,29 +772,6 @@ RSpec/LetSetup:
- 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
- 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
RSpec/MatchArray:
Exclude:
- 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb'
- 'spec/controllers/admin/export_domain_blocks_controller_spec.rb'
- 'spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb'
- 'spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb'
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
- 'spec/controllers/api/v1/bookmarks_controller_spec.rb'
- 'spec/controllers/api/v1/favourites_controller_spec.rb'
- 'spec/controllers/api/v1/reports_controller_spec.rb'
- 'spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb'
- 'spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb'
- 'spec/models/account_filter_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/account_statuses_cleanup_policy_spec.rb'
- 'spec/models/custom_emoji_filter_spec.rb'
- 'spec/models/status_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/presenters/familiar_followers_presenter_spec.rb'
- 'spec/services/activitypub/fetch_featured_collection_service_spec.rb'
- 'spec/services/update_status_service_spec.rb'
RSpec/MessageChain: RSpec/MessageChain:
Exclude: Exclude:
- 'spec/controllers/api/v1/media_controller_spec.rb' - 'spec/controllers/api/v1/media_controller_spec.rb'
@ -842,7 +811,6 @@ RSpec/MissingExampleGroupArgument:
- 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb' - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb'
- 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb' - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb'
- 'spec/controllers/api/v1/statuses_controller_spec.rb' - 'spec/controllers/api/v1/statuses_controller_spec.rb'
- 'spec/controllers/application_controller_spec.rb'
- 'spec/controllers/auth/registrations_controller_spec.rb' - 'spec/controllers/auth/registrations_controller_spec.rb'
- 'spec/features/log_in_spec.rb' - 'spec/features/log_in_spec.rb'
- 'spec/lib/activitypub/activity/undo_spec.rb' - 'spec/lib/activitypub/activity/undo_spec.rb'
@ -1225,9 +1193,6 @@ Rails/ActiveRecordCallbacksOrder:
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
- 'app/controllers/health_controller.rb' - 'app/controllers/health_controller.rb'
- 'app/controllers/well_known/host_meta_controller.rb'
- 'app/controllers/well_known/nodeinfo_controller.rb'
- 'app/controllers/well_known/webfinger_controller.rb'
# Configuration parameters: Database, Include. # Configuration parameters: Database, Include.
# SupportedDatabases: mysql, postgresql # SupportedDatabases: mysql, postgresql
@ -1405,14 +1370,6 @@ Rails/HasManyOrHasOneDependent:
- 'app/models/user.rb' - 'app/models/user.rb'
- 'app/models/web/push_subscription.rb' - 'app/models/web/push_subscription.rb'
# Configuration parameters: Include.
# Include: app/helpers/**/*.rb
Rails/HelperInstanceVariable:
Exclude:
- 'app/helpers/application_helper.rb'
- 'app/helpers/instance_helper.rb'
- 'app/helpers/jsonld_helper.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Include. # Configuration parameters: Include.
# Include: spec/**/*, test/**/* # Include: spec/**/*, test/**/*
@ -1502,15 +1459,6 @@ Rails/RakeEnvironment:
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
- 'lib/tasks/statistics.rake' - 'lib/tasks/statistics.rake'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Include.
# Include: spec/controllers/**/*.rb, spec/requests/**/*.rb, test/controllers/**/*.rb, test/integration/**/*.rb
Rails/ResponseParsedBody:
Exclude:
- 'spec/controllers/follower_accounts_controller_spec.rb'
- 'spec/controllers/following_accounts_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb'
# Configuration parameters: Include. # Configuration parameters: Include.
# Include: db/**/*.rb # Include: db/**/*.rb
Rails/ReversibleMigration: Rails/ReversibleMigration:
@ -2256,16 +2204,11 @@ Style/MapToHash:
# SupportedStyles: literals, strict # SupportedStyles: literals, strict
Style/MutableConstant: Style/MutableConstant:
Exclude: Exclude:
- 'app/lib/link_details_extractor.rb'
- 'app/models/account.rb' - 'app/models/account.rb'
- 'app/models/custom_emoji.rb'
- 'app/models/tag.rb' - 'app/models/tag.rb'
- 'app/services/account_search_service.rb'
- 'app/services/delete_account_service.rb' - 'app/services/delete_account_service.rb'
- 'app/services/fetch_link_card_service.rb'
- 'app/services/resolve_url_service.rb'
- 'config/initializers/twitter_regex.rb' - 'config/initializers/twitter_regex.rb'
- 'lib/mastodon/snowflake.rb' - 'lib/mastodon/migration_warning.rb'
- 'spec/controllers/api/base_controller_spec.rb' - 'spec/controllers/api/base_controller_spec.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
@ -2273,12 +2216,6 @@ Style/NilLambda:
Exclude: Exclude:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns.
Style/NumericLiterals:
Exclude:
- 'config/initializers/strong_migrations.rb'
# Configuration parameters: AllowedMethods. # Configuration parameters: AllowedMethods.
# AllowedMethods: respond_to_missing? # AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter: Style/OptionalBooleanParameter:
@ -2388,7 +2325,6 @@ Style/Semicolon:
Exclude: Exclude:
- 'spec/services/activitypub/process_status_update_service_spec.rb' - 'spec/services/activitypub/process_status_update_service_spec.rb'
- 'spec/validators/blacklisted_email_validator_spec.rb' - 'spec/validators/blacklisted_email_validator_spec.rb'
- 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 2.7.0', '< 3.3.0' ruby '>= 3.0.0'
gem 'pkg-config', '~> 1.5' gem 'pkg-config', '~> 1.5'
@ -9,10 +9,10 @@ gem 'puma', '~> 6.2'
gem 'rails', '~> 6.1.7' gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2' gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.6' gem 'rack', '~> 2.2.7'
gem 'haml-rails', '~>2.0' gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.4' gem 'pg', '~> 1.5'
gem 'makara', '~> 0.5' gem 'makara', '~> 0.5'
gem 'pghero' gem 'pghero'
gem 'dotenv-rails', '~> 2.8' gem 'dotenv-rails', '~> 2.8'
@ -30,7 +30,10 @@ 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.0' # The below `v4.x` branch allows attr_encrypted 4.x, which is required for Rails 7.
# Once a new gem version is pushed, we can go back to released gem and off of github branch.
gem 'devise-two-factor', github: 'tinfoil/devise-two-factor', branch: 'v4.x'
gem 'attr_encrypted', '~> 4.0'
group :pam_authentication, optional: true do group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2' gem 'devise_pam_authenticatable2', '~> 9.2'
@ -76,7 +79,7 @@ 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 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.1' gem 'rqrcode', '~> 2.1'
gem 'ruby-progressbar', '~> 1.11' 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'
@ -121,7 +124,7 @@ group :test do
gem 'capybara', '~> 3.39' gem 'capybara', '~> 3.39'
gem 'climate_control' gem 'climate_control'
gem 'faker', '~> 3.2' gem 'faker', '~> 3.2'
gem 'json-schema', '~> 3.0' gem 'json-schema', '~> 4.0'
gem 'rack-test', '~> 2.1' gem 'rack-test', '~> 2.1'
gem 'rails-controller-testing', '~> 1.0' gem 'rails-controller-testing', '~> 1.0'
gem 'rspec_junit_formatter', '~> 0.6' gem 'rspec_junit_formatter', '~> 0.6'

@ -27,6 +27,18 @@ GIT
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) rails (>= 4.2.0)
GIT
remote: https://github.com/tinfoil/devise-two-factor.git
revision: e685f91ce62d036259885fbe31fcb4fa930bcfcb
branch: v4.x
specs:
devise-two-factor (4.0.2)
activesupport (< 7.1)
attr_encrypted (>= 1.3, < 5, != 2)
devise (~> 4.0)
railties (< 7.1)
rotp (~> 6.0)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
@ -104,12 +116,12 @@ 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 (3.1.0) attr_encrypted (4.0.0)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
attr_required (1.0.1) attr_required (1.0.1)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.743.0) aws-partitions (1.752.0)
aws-sdk-core (3.171.0) aws-sdk-core (3.171.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
@ -118,7 +130,7 @@ GEM
aws-sdk-kms (1.63.0) aws-sdk-kms (1.63.0)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.120.1) aws-sdk-s3 (1.121.0)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
@ -142,7 +154,7 @@ GEM
blurhash (0.1.7) blurhash (0.1.7)
bootsnap (1.16.0) bootsnap (1.16.0)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (5.4.0) brakeman (5.4.1)
browser (5.3.1) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
@ -156,7 +168,7 @@ GEM
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
sshkit (>= 1.9.0) sshkit (>= 1.9.0)
capistrano-bundler (2.0.1) capistrano-bundler (2.1.0)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-rails (1.6.2) capistrano-rails (1.6.2)
capistrano (~> 3.1) capistrano (~> 3.1)
@ -179,7 +191,7 @@ GEM
activesupport activesupport
cbor (0.5.9.6) cbor (0.5.9.6)
charlock_holmes (0.7.7) charlock_holmes (0.7.7)
chewy (7.3.0) chewy (7.3.2)
activesupport (>= 5.2) activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl elasticsearch-dsl
@ -189,29 +201,23 @@ GEM
coderay (1.1.3) coderay (1.1.3)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.2.2) concurrent-ruby (1.2.2)
connection_pool (2.3.0) connection_pool (2.4.0)
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.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.12.0) css_parser (1.14.0)
addressable addressable
date (3.3.3) date (3.3.3)
debug_inspector (1.0.0) debug_inspector (1.1.0)
devise (4.9.2) devise (4.9.2)
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.0.2)
activesupport (< 7.1)
attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0)
railties (< 7.1)
rotp (~> 6.0)
devise_pam_authenticatable2 (9.2.0) devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0) devise (>= 4.0.0)
rpam2 (~> 4.0) rpam2 (~> 4.0)
@ -241,7 +247,7 @@ GEM
erubi (1.12.0) erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
excon (0.95.0) excon (0.99.0)
fabrication (2.30.0) fabrication (2.30.0)
faker (3.2.0) faker (3.2.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
@ -314,7 +320,7 @@ GEM
hashie (5.0.0) hashie (5.0.0)
hcaptcha (7.1.0) hcaptcha (7.1.0)
json json
highline (2.0.3) highline (2.1.0)
hiredis (0.6.3) hiredis (0.6.3)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
@ -364,7 +370,7 @@ GEM
json-ld-preloaded (3.2.2) json-ld-preloaded (3.2.2)
json-ld (~> 3.2) json-ld (~> 3.2)
rdf (~> 3.2) rdf (~> 3.2)
json-schema (3.0.0) json-schema (4.0.0)
addressable (>= 2.8) addressable (>= 2.8)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.7.0) jwt (2.7.0)
@ -380,8 +386,8 @@ GEM
activerecord activerecord
kaminari-core (= 1.2.2) kaminari-core (= 1.2.2)
kaminari-core (1.2.2) kaminari-core (1.2.2)
launchy (2.5.0) launchy (2.5.2)
addressable (~> 2.7) addressable (~> 2.8)
letter_opener (1.8.1) letter_opener (1.8.1)
launchy (>= 2.2, < 3) launchy (>= 2.2, < 3)
letter_opener_web (2.0.0) letter_opener_web (2.0.0)
@ -416,11 +422,11 @@ GEM
method_source (1.0.0) method_source (1.0.0)
mime-types (3.4.1) mime-types (3.4.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105) mime-types-data (3.2023.0218.1)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.1) mini_portile2 (2.8.1)
minitest (5.18.0) minitest (5.18.0)
msgpack (1.6.0) msgpack (1.7.0)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
net-http (0.3.2) net-http (0.3.2)
@ -437,7 +443,7 @@ GEM
net-ssh (>= 2.6.5, < 8.0.0) net-ssh (>= 2.6.5, < 8.0.0)
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
net-ssh (7.0.1) net-ssh (7.1.0)
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.14.3) nokogiri (1.14.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
@ -480,18 +486,18 @@ GEM
openssl (> 2.0) openssl (> 2.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.14.16) ox (2.14.16)
parallel (1.22.1) parallel (1.23.0)
parser (3.2.2.0) parser (3.2.2.1)
ast (~> 2.4.1) ast (~> 2.4.1)
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.4.6) pg (1.5.2)
pghero (3.3.2) pghero (3.3.3)
activerecord (>= 6) activerecord (>= 6)
pkg-config (1.5.1) pkg-config (1.5.1)
posix-spawn (0.3.15) posix-spawn (0.3.15)
premailer (1.18.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)
@ -501,13 +507,13 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0) private_address_check (0.5.0)
public_suffix (5.0.1) public_suffix (5.0.1)
puma (6.2.1) puma (6.2.2)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.2) racc (1.6.2)
rack (2.2.6.4) rack (2.2.7)
rack-attack (6.6.1) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (2.0.1) rack-cors (2.0.1)
@ -567,25 +573,25 @@ GEM
redis (>= 4) redis (>= 4)
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.7.0) regexp_parser (2.8.0)
request_store (1.5.1) request_store (1.5.1)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.0) responders (3.1.0)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.2.5) rexml (3.2.5)
rotp (6.2.0) rotp (6.2.2)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (2.1.2) rqrcode (2.1.2)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.12.1) rspec-core (3.12.2)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-expectations (3.12.2) rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-mocks (3.12.3) rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-rails (6.0.1) rspec-rails (6.0.1)
@ -603,7 +609,7 @@ GEM
rspec_chunked (0.6) rspec_chunked (0.6)
rspec_junit_formatter (0.6.0) rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0) rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.49.0) rubocop (1.50.2)
json (~> 2.3) json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.0.0) parser (>= 3.2.0.0)
@ -615,7 +621,7 @@ GEM
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.0) rubocop-ast (1.28.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.17.1) rubocop-capybara (2.18.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-performance (1.17.1) rubocop-performance (1.17.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
@ -771,6 +777,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10) active_model_serializers (~> 0.10)
addressable (~> 2.8) addressable (~> 2.8)
annotate (~> 3.2) annotate (~> 3.2)
attr_encrypted (~> 4.0)
aws-sdk-s3 (~> 1.120) aws-sdk-s3 (~> 1.120)
better_errors (~> 2.9) better_errors (~> 2.9)
binding_of_caller (~> 1.0) binding_of_caller (~> 1.0)
@ -792,7 +799,7 @@ DEPENDENCIES
concurrent-ruby concurrent-ruby
connection_pool connection_pool
devise (~> 4.9) devise (~> 4.9)
devise-two-factor (~> 4.0) 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)
@ -817,7 +824,7 @@ DEPENDENCIES
idn-ruby idn-ruby
json-ld json-ld
json-ld-preloaded (~> 3.2) json-ld-preloaded (~> 3.2)
json-schema (~> 3.0) json-schema (~> 4.0)
kaminari (~> 1.2) kaminari (~> 1.2)
kt-paperclip (~> 7.1)! kt-paperclip (~> 7.1)!
letter_opener (~> 1.8) letter_opener (~> 1.8)
@ -840,7 +847,7 @@ DEPENDENCIES
omniauth_openid_connect (~> 0.6.1) omniauth_openid_connect (~> 0.6.1)
ox (~> 2.14) ox (~> 2.14)
parslet parslet
pg (~> 1.4) pg (~> 1.5)
pghero pghero
pkg-config (~> 1.5) pkg-config (~> 1.5)
posix-spawn posix-spawn
@ -849,7 +856,7 @@ DEPENDENCIES
public_suffix (~> 5.0) public_suffix (~> 5.0)
puma (~> 6.2) puma (~> 6.2)
pundit (~> 2.3) pundit (~> 2.3)
rack (~> 2.2.6) rack (~> 2.2.7)
rack-attack (~> 6.6) rack-attack (~> 6.6)
rack-cors (~> 2.0) rack-cors (~> 2.0)
rack-test (~> 2.1) rack-test (~> 2.1)
@ -871,7 +878,7 @@ DEPENDENCIES
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
ruby-progressbar (~> 1.11) ruby-progressbar (~> 1.13)
sanitize (~> 6.0) sanitize (~> 6.0)
scenic (~> 1.7) scenic (~> 1.7)
sidekiq (~> 6.5) sidekiq (~> 6.5)

@ -8,7 +8,7 @@ class AboutController < ApplicationController
before_action :set_instance_presenter before_action :set_instance_presenter
def show def show
expires_in 0, public: true unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end end
private private

@ -7,8 +7,9 @@ class AccountsController < ApplicationController
include AccountControllerConcern include AccountControllerConcern
include SignatureAuthentication include SignatureAuthentication
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :whitelist_mode?
@ -16,7 +17,7 @@ class AccountsController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
expires_in 0, public: true unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in?
@rss_url = rss_url @rss_url = rss_url
end end

@ -7,10 +7,6 @@ class ActivityPub::BaseController < Api::BaseController
private private
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode?
end
def skip_temporary_suspension_response? def skip_temporary_suspension_response?
false false
end end

@ -4,11 +4,12 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
include SignatureVerification include SignatureVerification
include AccountOwnedConcern include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_items before_action :set_items
before_action :set_size before_action :set_size
before_action :set_type before_action :set_type
before_action :set_cache_headers
def show def show
expires_in 3.minutes, public: public_fetch_mode? expires_in 3.minutes, public: public_fetch_mode?

@ -4,9 +4,10 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
include SignatureVerification include SignatureVerification
include AccountOwnedConcern include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature! before_action :require_account_signature!
before_action :set_items before_action :set_items
before_action :set_cache_headers
def show def show
expires_in 0, public: false expires_in 0, public: false

@ -6,9 +6,10 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
include SignatureVerification include SignatureVerification
include AccountOwnedConcern include AccountOwnedConcern
vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? }
before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_statuses before_action :set_statuses
before_action :set_cache_headers
def show def show
if page_requested? if page_requested?
@ -16,6 +17,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
else else
expires_in(3.minutes, public: public_fetch_mode?) expires_in(3.minutes, public: public_fetch_mode?)
end end
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end end
@ -80,8 +82,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def set_account def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end end
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
end
end end

@ -7,9 +7,10 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
DESCENDANTS_LIMIT = 60 DESCENDANTS_LIMIT = 60
vary_by -> { 'Signature' if authorized_fetch_mode? }
before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :require_account_signature!, if: :authorized_fetch_mode?
before_action :set_status before_action :set_status
before_action :set_cache_headers
before_action :set_replies before_action :set_replies
def index def index

@ -9,6 +9,8 @@ module Admin
before_action :set_pack before_action :set_pack
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
after_action :verify_authorized after_action :verify_authorized
private private
@ -21,6 +23,10 @@ module Admin
use_pack 'admin' use_pack 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
def set_user def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end end

@ -6,13 +6,14 @@ class Api::BaseController < ApplicationController
include RateLimitHeaders include RateLimitHeaders
include AccessTokenTrackingConcern include AccessTokenTrackingConcern
include ApiCachingConcern
skip_before_action :store_current_location
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :require_not_suspended! before_action :require_not_suspended!
before_action :set_cache_headers
vary_by 'Authorization'
protect_from_forgery with: :null_session protect_from_forgery with: :null_session
@ -148,10 +149,6 @@ class Api::BaseController < ApplicationController
doorkeeper_authorize!(*scopes) if doorkeeper_token doorkeeper_authorize!(*scopes) if doorkeeper_token
end end
def set_cache_headers
response.headers['Cache-Control'] = 'private, no-store'
end
def disallow_unauthenticated_api_access? def disallow_unauthenticated_api_access?
ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode
end end

@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
cache_if_unauthenticated!
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer
end end

@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
cache_if_unauthenticated!
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer
end end

@ -5,6 +5,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController
before_action :set_account before_action :set_account
def show def show
cache_if_unauthenticated!
render json: @account, serializer: REST::AccountSerializer render json: @account, serializer: REST::AccountSerializer
end end

@ -7,6 +7,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
def index def index
cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end end

@ -18,6 +18,7 @@ class Api::V1::AccountsController < Api::BaseController
override_rate_limit_headers :follow, family: :follows override_rate_limit_headers :follow, family: :follows
def show def show
cache_if_unauthenticated!
render json: @account, serializer: REST::AccountSerializer render json: @account, serializer: REST::AccountSerializer
end end

@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::CustomEmojisController < Api::BaseController class Api::V1::CustomEmojisController < Api::BaseController
skip_before_action :set_cache_headers vary_by '', unless: :disallow_unauthenticated_api_access?
def index def index
expires_in 3.minutes, public: true cache_even_if_authenticated! unless disallow_unauthenticated_api_access?
render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) } render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) }
end end
end end

@ -5,6 +5,7 @@ class Api::V1::DirectoriesController < Api::BaseController
before_action :set_accounts before_action :set_accounts
def show def show
cache_if_unauthenticated!
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer
end end

@ -3,11 +3,12 @@
class Api::V1::Instances::ActivityController < Api::BaseController class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
vary_by ''
def show def show
expires_in 1.day, public: true cache_even_if_authenticated!
render_with_cache json: :activity, expires_in: 1.day render_with_cache json: :activity, expires_in: 1.day
end end

@ -6,8 +6,15 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
before_action :set_domain_blocks before_action :set_domain_blocks
vary_by '', if: -> { Setting.show_domain_blocks == 'all' }
def index def index
expires_in 3.minutes, public: true if Setting.show_domain_blocks == 'all'
cache_even_if_authenticated!
else
cache_if_unauthenticated!
end
render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?))
end end

@ -2,11 +2,19 @@
class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
skip_around_action :set_locale
before_action :set_extended_description before_action :set_extended_description
vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user
super if whitelist_mode?
end
def show def show
expires_in 3.minutes, public: true cache_even_if_authenticated!
render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer
end end

@ -3,11 +3,18 @@
class Api::V1::Instances::PeersController < Api::BaseController class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
skip_around_action :set_locale
vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user
super if whitelist_mode?
end
def index def index
expires_in 1.day, public: true cache_even_if_authenticated!
render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) } render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) }
end end

@ -5,8 +5,10 @@ class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController
before_action :set_privacy_policy before_action :set_privacy_policy
vary_by ''
def show def show
expires_in 1.day, public: true cache_even_if_authenticated!
render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer
end end

@ -2,10 +2,19 @@
class Api::V1::Instances::RulesController < Api::BaseController class Api::V1::Instances::RulesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
skip_around_action :set_locale
before_action :set_rules before_action :set_rules
vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user
super if whitelist_mode?
end
def index def index
cache_even_if_authenticated!
render json: @rules, each_serializer: REST::RuleSerializer render json: @rules, each_serializer: REST::RuleSerializer
end end

@ -5,8 +5,10 @@ class Api::V1::Instances::TranslationLanguagesController < Api::BaseController
before_action :set_languages before_action :set_languages
vary_by ''
def show def show
expires_in 1.day, public: true cache_even_if_authenticated!
render json: @languages render json: @languages
end end

@ -1,11 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::InstancesController < Api::BaseController class Api::V1::InstancesController < Api::BaseController
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode? skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
skip_around_action :set_locale
vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user
super if whitelist_mode?
end
def show def show
expires_in 3.minutes, public: true cache_even_if_authenticated!
render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance' render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance'
end end
end end

@ -8,6 +8,7 @@ class Api::V1::PollsController < Api::BaseController
before_action :refresh_poll before_action :refresh_poll
def show def show
cache_if_unauthenticated!
render json: @poll, serializer: REST::PollSerializer, include_results: true render json: @poll, serializer: REST::PollSerializer, include_results: true
end end

@ -8,6 +8,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
cache_if_unauthenticated!
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer
end end

@ -7,6 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
before_action :set_status before_action :set_status
def show def show
cache_if_unauthenticated!
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
end end

@ -8,6 +8,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
cache_if_unauthenticated!
@accounts = load_accounts @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer
end end

@ -24,11 +24,14 @@ class Api::V1::StatusesController < Api::BaseController
DESCENDANTS_DEPTH_LIMIT = 20 DESCENDANTS_DEPTH_LIMIT = 20
def show def show
cache_if_unauthenticated!
@status = cache_collection([@status], Status).first @status = cache_collection([@status], Status).first
render json: @status, serializer: REST::StatusSerializer render json: @status, serializer: REST::StatusSerializer
end end
def context def context
cache_if_unauthenticated!
ancestors_limit = CONTEXT_LIMIT ancestors_limit = CONTEXT_LIMIT
descendants_limit = CONTEXT_LIMIT descendants_limit = CONTEXT_LIMIT
descendants_depth_limit = nil descendants_depth_limit = nil

@ -8,6 +8,7 @@ class Api::V1::TagsController < Api::BaseController
override_rate_limit_headers :follow, family: :follows override_rate_limit_headers :follow, family: :follows
def show def show
cache_if_unauthenticated!
render json: @tag, serializer: REST::TagSerializer render json: @tag, serializer: REST::TagSerializer
end end

@ -5,6 +5,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show def show
cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end end

@ -5,6 +5,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show def show
cache_if_unauthenticated!
@statuses = load_statuses @statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end end

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Trends::LinksController < Api::BaseController class Api::V1::Trends::LinksController < Api::BaseController
vary_by 'Authorization, Accept-Language'
before_action :set_links before_action :set_links
after_action :insert_pagination_headers after_action :insert_pagination_headers
@ -8,6 +10,7 @@ class Api::V1::Trends::LinksController < Api::BaseController
DEFAULT_LINKS_LIMIT = 10 DEFAULT_LINKS_LIMIT = 10
def index def index
cache_if_unauthenticated!
render json: @links, each_serializer: REST::Trends::LinkSerializer render json: @links, each_serializer: REST::Trends::LinkSerializer
end end

@ -1,11 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Trends::StatusesController < Api::BaseController class Api::V1::Trends::StatusesController < Api::BaseController
vary_by 'Authorization, Accept-Language'
before_action :set_statuses before_action :set_statuses
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
cache_if_unauthenticated!
render json: @statuses, each_serializer: REST::StatusSerializer render json: @statuses, each_serializer: REST::StatusSerializer
end end

@ -8,6 +8,7 @@ class Api::V1::Trends::TagsController < Api::BaseController
DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i
def index def index
cache_if_unauthenticated!
render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id) render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id)
end end

@ -2,7 +2,7 @@
class Api::V2::InstancesController < Api::V1::InstancesController class Api::V2::InstancesController < Api::V1::InstancesController
def show def show
expires_in 3.minutes, public: true cache_even_if_authenticated!
render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'
end end
end end

@ -21,6 +21,8 @@ class ApplicationController < ActionController::Base
helper_method :omniauth_only? helper_method :omniauth_only?
helper_method :sso_account_settings helper_method :sso_account_settings
helper_method :whitelist_mode? helper_method :whitelist_mode?
helper_method :body_class_string
helper_method :skip_csrf_meta_tags?
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
rescue_from Mastodon::NotPermittedError, with: :forbidden rescue_from Mastodon::NotPermittedError, with: :forbidden
@ -37,9 +39,11 @@ class ApplicationController < ActionController::Base
service_unavailable service_unavailable
end end
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :store_referrer, except: :raise_not_found, if: :devise_controller?
before_action :require_functional!, if: :user_signed_in? before_action :require_functional!, if: :user_signed_in?
before_action :set_cache_control_defaults
skip_before_action :verify_authenticity_token, only: :raise_not_found skip_before_action :verify_authenticity_token, only: :raise_not_found
def raise_not_found def raise_not_found
@ -56,14 +60,25 @@ class ApplicationController < ActionController::Base
!authorized_fetch_mode? !authorized_fetch_mode?
end end
def store_current_location def store_referrer
store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym) return if request.referer.blank?
redirect_uri = URI(request.referer)
return if redirect_uri.path.start_with?('/auth')
stored_url = redirect_uri.to_s if redirect_uri.host == request.host && redirect_uri.port == request.port
store_location_for(:user, stored_url)
end end
def require_functional! def require_functional!
redirect_to edit_user_registration_path unless current_user.functional? redirect_to edit_user_registration_path unless current_user.functional?
end end
def skip_csrf_meta_tags?
false
end
def after_sign_out_path_for(_resource_or_scope) def after_sign_out_path_for(_resource_or_scope)
if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true' if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true'
'/auth/auth/openid_connect/logout' '/auth/auth/openid_connect/logout'
@ -127,7 +142,7 @@ class ApplicationController < ActionController::Base
end end
def sso_account_settings def sso_account_settings
ENV.fetch('SSO_ACCOUNT_SETTINGS') ENV.fetch('SSO_ACCOUNT_SETTINGS', nil)
end end
def current_account def current_account
@ -142,6 +157,10 @@ class ApplicationController < ActionController::Base
@current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present? @current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present?
end end
def body_class_string
@body_classes || ''
end
def respond_with_error(code) def respond_with_error(code)
respond_to do |format| respond_to do |format|
format.any do format.any do
@ -151,4 +170,8 @@ class ApplicationController < ActionController::Base
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
def set_cache_control_defaults
response.cache_control.replace(private: true, no_store: true)
end
end end

@ -157,6 +157,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end
def set_cache_headers def set_cache_headers
response.headers['Cache-Control'] = 'private, no-store' response.cache_control.replace(private: true, no_store: true)
end end
end end

@ -0,0 +1,13 @@
# frozen_string_literal: true
module ApiCachingConcern
extend ActiveSupport::Concern
def cache_if_unauthenticated!
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end
def cache_even_if_authenticated!
expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless whitelist_mode?
end
end

@ -155,8 +155,30 @@ module CacheConcern
end end
end end
class_methods do
def vary_by(value, **kwargs)
before_action(**kwargs) do |controller|
response.headers['Vary'] = value.respond_to?(:call) ? controller.instance_exec(&value) : value
end
end
end
included do
after_action :enforce_cache_control!
end
# Prevents high-entropy headers such as `Cookie`, `Signature` or `Authorization`
# from being used as cache keys, while allowing to `Vary` on them (to not serve
# anonymous cached data to authenticated requests when authentication matters)
def enforce_cache_control!
vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase }
return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? }
response.cache_control.replace(private: true, no_store: true)
end
def render_with_cache(**options) def render_with_cache(**options)
raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given? raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given?
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
expires_in = options.delete(:expires_in) || 3.minutes expires_in = options.delete(:expires_in) || 3.minutes
@ -176,10 +198,6 @@ module CacheConcern
end end
end end
def set_cache_headers
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
end
def cache_collection(raw, klass) def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes) return raw unless klass.respond_to?(:with_includes)

@ -7,6 +7,12 @@ module WebAppControllerConcern
prepend_before_action :redirect_unauthenticated_to_permalinks! prepend_before_action :redirect_unauthenticated_to_permalinks!
before_action :set_pack before_action :set_pack
before_action :set_app_body_class before_action :set_app_body_class
vary_by 'Accept, Accept-Language, Cookie'
end
def skip_csrf_meta_tags?
current_user.nil?
end end
def set_app_body_class def set_app_body_class

@ -1,18 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class CustomCssController < ApplicationController class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
skip_before_action :store_current_location
skip_before_action :require_functional!
skip_before_action :update_user_sign_in
skip_before_action :set_session_activity
skip_around_action :set_locale
before_action :set_cache_headers
def show def show
expires_in 3.minutes, public: true expires_in 3.minutes, public: true
request.session_options[:skip] = true
render content_type: 'text/css' render content_type: 'text/css'
end end
end end

@ -10,6 +10,7 @@ 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_pack
before_action :set_cache_headers
private private
@ -20,4 +21,8 @@ class Disputes::BaseController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

@ -2,15 +2,12 @@
class EmojisController < ApplicationController class EmojisController < ApplicationController
before_action :set_emoji before_action :set_emoji
before_action :set_cache_headers
vary_by -> { 'Signature' if authorized_fetch_mode? }
def show def show
respond_to do |format| expires_in 3.minutes, public: true
format.json do render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
expires_in 3.minutes, public: true
render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter
end
end
end end
private private

@ -8,6 +8,7 @@ class Filters::StatusesController < ApplicationController
before_action :set_status_filters before_action :set_status_filters
before_action :set_pack before_action :set_pack
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
PER_PAGE = 20 PER_PAGE = 20
@ -49,4 +50,8 @@ class Filters::StatusesController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

@ -7,6 +7,7 @@ class FiltersController < ApplicationController
before_action :set_filter, only: [:edit, :update, :destroy] before_action :set_filter, only: [:edit, :update, :destroy]
before_action :set_pack before_action :set_pack
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
def index def index
@filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase)
@ -59,4 +60,8 @@ class FiltersController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

@ -5,8 +5,9 @@ class FollowerAccountsController < ApplicationController
include SignatureVerification include SignatureVerification
include WebAppControllerConcern include WebAppControllerConcern
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json } skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :whitelist_mode?
@ -14,7 +15,7 @@ class FollowerAccountsController < ApplicationController
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
expires_in 0, public: true unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in?
end end
format.json do format.json do

@ -5,8 +5,9 @@ class FollowingAccountsController < ApplicationController
include SignatureVerification include SignatureVerification
include WebAppControllerConcern include WebAppControllerConcern
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json } skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :whitelist_mode?
@ -14,7 +15,7 @@ class FollowingAccountsController < ApplicationController
def index def index
respond_to do |format| respond_to do |format|
format.html do format.html do
expires_in 0, public: true unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in?
end end
format.json do format.json do

@ -6,7 +6,7 @@ class HomeController < ApplicationController
before_action :set_instance_presenter before_action :set_instance_presenter
def index def index
expires_in 0, public: true unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end end
private private

@ -1,10 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class InstanceActorsController < ApplicationController class InstanceActorsController < ActivityPub::BaseController
include AccountControllerConcern vary_by ''
skip_before_action :check_account_confirmation serialization_scope nil
skip_around_action :set_locale
before_action :set_account
skip_before_action :require_functional!
skip_before_action :update_user_sign_in
def show def show
expires_in 10.minutes, public: true expires_in 10.minutes, public: true

@ -8,6 +8,7 @@ class InvitesController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_pack before_action :set_pack
before_action :set_body_classes before_action :set_body_classes
before_action :set_cache_headers
def index def index
authorize :invite, :create? authorize :invite, :create?
@ -54,4 +55,8 @@ class InvitesController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

@ -1,8 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
class ManifestsController < ApplicationController class ManifestsController < ActionController::Base # rubocop:disable Rails/ApplicationController
skip_before_action :store_current_location # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user`
skip_before_action :require_functional! # and thus re-issuing session cookies
serialization_scope nil
def show def show
expires_in 3.minutes, public: true expires_in 3.minutes, public: true

@ -3,7 +3,6 @@
class MediaController < ApplicationController class MediaController < ApplicationController
include Authorization include Authorization
skip_before_action :store_current_location
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :whitelist_mode?
before_action :authenticate_user!, if: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode?

@ -6,7 +6,6 @@ class MediaProxyController < ApplicationController
include Redisable include Redisable
include Lockable include Lockable
skip_before_action :store_current_location
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :authenticate_user!, if: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode?

@ -39,6 +39,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
end end
def set_cache_headers def set_cache_headers
response.headers['Cache-Control'] = 'private, no-store' response.cache_control.replace(private: true, no_store: true)
end end
end end

@ -8,6 +8,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
before_action :set_pack 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
skip_before_action :require_functional! skip_before_action :require_functional!
@ -35,4 +36,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
def require_not_suspended! def require_not_suspended!
forbidden if current_account.suspended? forbidden if current_account.suspended?
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

@ -8,7 +8,7 @@ class PrivacyController < ApplicationController
before_action :set_instance_presenter before_action :set_instance_presenter
def show def show
expires_in 0, public: true if current_account.nil? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
end end
private private

@ -8,6 +8,7 @@ class RelationshipsController < ApplicationController
before_action :set_pack 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
helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
@ -75,4 +76,8 @@ class RelationshipsController < ApplicationController
def set_pack def set_pack
use_pack 'admin' use_pack 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

@ -19,7 +19,7 @@ class Settings::BaseController < ApplicationController
end end
def set_cache_headers def set_cache_headers
response.headers['Cache-Control'] = 'private, no-store' response.cache_control.replace(private: true, no_store: true)
end end
def require_not_suspended! def require_not_suspended!

@ -7,6 +7,7 @@ class StatusesCleanupController < ApplicationController
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_pack
before_action :set_cache_headers
def show; end def show; end
@ -41,4 +42,8 @@ class StatusesCleanupController < ApplicationController
def set_body_classes def set_body_classes
@body_classes = 'admin' @body_classes = 'admin'
end end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end end

@ -6,11 +6,12 @@ class StatusesController < ApplicationController
include Authorization include Authorization
include AccountOwnedConcern include AccountOwnedConcern
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status before_action :set_status
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :redirect_to_original, only: :show before_action :redirect_to_original, only: :show
before_action :set_cache_headers
before_action :set_body_classes, only: :embed before_action :set_body_classes, only: :embed
after_action :set_link_headers after_action :set_link_headers
@ -29,7 +30,7 @@ class StatusesController < ApplicationController
end end
format.json do format.json do
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode?
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter
end end
end end

@ -7,6 +7,8 @@ class TagsController < ApplicationController
PAGE_SIZE = 20 PAGE_SIZE = 20
PAGE_SIZE_MAX = 200 PAGE_SIZE_MAX = 200
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_local before_action :set_local
@ -19,7 +21,7 @@ class TagsController < ApplicationController
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html do
expires_in 0, public: true unless user_signed_in? expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in?
end end
format.rss do format.rss do

@ -1,11 +1,9 @@
# frozen_string_literal: true # frozen_string_literal: true
module WellKnown module WellKnown
class HostMetaController < ActionController::Base class HostMetaController < ActionController::Base # rubocop:disable Rails/ApplicationController
include RoutingHelper include RoutingHelper
before_action { response.headers['Vary'] = 'Accept' }
def show def show
@webfinger_template = "#{webfinger_url}?resource={uri}" @webfinger_template = "#{webfinger_url}?resource={uri}"
expires_in 3.days, public: true expires_in 3.days, public: true

@ -1,10 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
module WellKnown module WellKnown
class NodeInfoController < ActionController::Base class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController
include CacheConcern include CacheConcern
before_action { response.headers['Vary'] = 'Accept' } # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user`
# and thus re-issuing session cookies
serialization_scope nil
def index def index
expires_in 3.days, public: true expires_in 3.days, public: true

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module WellKnown module WellKnown
class WebfingerController < ActionController::Base class WebfingerController < ActionController::Base # rubocop:disable Rails/ApplicationController
include RoutingHelper include RoutingHelper
before_action :set_account before_action :set_account
@ -34,7 +34,12 @@ module WellKnown
end end
def check_account_suspension def check_account_suspension
expires_in(3.minutes, public: true) && gone if @account.suspended_permanently? gone if @account.suspended_permanently?
end
def gone
expires_in(3.minutes, public: true)
head 410
end end
def bad_request def bad_request
@ -46,9 +51,5 @@ module WellKnown
expires_in(3.minutes, public: true) expires_in(3.minutes, public: true)
head 404 head 404
end end
def gone
head 410
end
end end
end end

@ -155,20 +155,8 @@ module ApplicationHelper
tag(:meta, content: content, property: property) tag(:meta, content: content, property: property)
end end
def react_component(name, props = {}, &block)
if block.nil?
content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) })
else
content_tag(:div, data: { component: name.to_s.camelcase, props: Oj.dump(props) }, &block)
end
end
def react_admin_component(name, props = {})
content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) })
end
def body_classes def body_classes
output = (@body_classes || '').split output = body_class_string.split
output << "flavour-#{current_flavour.parameterize}" output << "flavour-#{current_flavour.parameterize}"
output << "skin-#{current_skin.parameterize}" output << "skin-#{current_skin.parameterize}"
output << 'system-font' if current_account&.user&.setting_system_font_ui output << 'system-font' if current_account&.user&.setting_system_font_ui

@ -9,13 +9,17 @@ module InstanceHelper
@site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host @site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host
end end
def description_for_sign_up def description_for_sign_up(invite = nil)
prefix = if @invite.present? safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ')
I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username) end
else
I18n.t('auth.description.prefix_sign_up') private
end
safe_join([prefix, I18n.t('auth.description.suffix')], ' ') def description_prefix(invite)
if invite.present?
I18n.t('auth.description.prefix_invited_by_user', name: invite.user.account.username)
else
I18n.t('auth.description.prefix_sign_up')
end
end end
end end

@ -63,11 +63,11 @@ module JsonLdHelper
uri.nil? || !uri.start_with?('http://', 'https://') uri.nil? || !uri.start_with?('http://', 'https://')
end end
def invalid_origin?(url) def non_matching_uri_hosts?(base_url, comparison_url)
return true if unsupported_uri_scheme?(url) return true if unsupported_uri_scheme?(comparison_url)
needle = Addressable::URI.parse(url).host needle = Addressable::URI.parse(comparison_url).host
haystack = Addressable::URI.parse(@account.uri).host haystack = Addressable::URI.parse(base_url).host
!haystack.casecmp(needle).zero? !haystack.casecmp(needle).zero?
end end

@ -201,7 +201,6 @@ module LanguagesHelper
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
smj: ['Lule Sami', 'Julevsámegiella'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze,
szl: ['Silesian', 'ślůnsko godka'].freeze, szl: ['Silesian', 'ślůnsko godka'].freeze,
tai: ['Tai', 'ภาษาไท or ภาษาไต'].freeze,
tok: ['Toki Pona', 'toki pona'].freeze, tok: ['Toki Pona', 'toki pona'].freeze,
zba: ['Balaibalan', 'باليبلن'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze,
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,

@ -0,0 +1,111 @@
# frozen_string_literal: true
module MediaComponentHelper
def render_video_component(status, **options)
video = status.ordered_media_attachments.first
meta = video.file.meta || {}
component_params = {
sensitive: sensitive_viewer?(status, current_account),
src: full_asset_url(video.file.url(:original)),
preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)),
alt: video.description,
blurhash: video.blurhash,
frameRate: meta.dig('original', 'frame_rate'),
inline: true,
media: [
serialize_media_attachment(video),
].as_json,
}.merge(**options)
react_component :video, component_params do
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
def render_audio_component(status, **options)
audio = status.ordered_media_attachments.first
meta = audio.file.meta || {}
component_params = {
src: full_asset_url(audio.file.url(:original)),
poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url),
alt: audio.description,
backgroundColor: meta.dig('colors', 'background'),
foregroundColor: meta.dig('colors', 'foreground'),
accentColor: meta.dig('colors', 'accent'),
duration: meta.dig('original', 'duration'),
}.merge(**options)
react_component :audio, component_params do
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
def render_media_gallery_component(status, **options)
component_params = {
sensitive: sensitive_viewer?(status, current_account),
autoplay: prefers_autoplay?,
media: status.ordered_media_attachments.map { |a| serialize_media_attachment(a).as_json },
}.merge(**options)
react_component :media_gallery, component_params do
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
def render_card_component(status, **options)
component_params = {
sensitive: sensitive_viewer?(status, current_account),
card: serialize_status_card(status).as_json,
}.merge(**options)
react_component :card, component_params
end
def render_poll_component(status, **options)
component_params = {
disabled: true,
poll: serialize_status_poll(status).as_json,
}.merge(**options)
react_component :poll, component_params do
render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? }
end
end
private
def serialize_media_attachment(attachment)
ActiveModelSerializers::SerializableResource.new(
attachment,
serializer: REST::MediaAttachmentSerializer
)
end
def serialize_status_card(status)
ActiveModelSerializers::SerializableResource.new(
status.preview_card,
serializer: REST::PreviewCardSerializer
)
end
def serialize_status_poll(status)
ActiveModelSerializers::SerializableResource.new(
status.preloadable_poll,
serializer: REST::PollSerializer,
scope: current_user,
scope_name: :current_user
)
end
def sensitive_viewer?(status, account)
if !account.nil? && account.id == status.account_id
status.sensitive
else
status.account.sensitized? || status.sensitive
end
end
end

@ -0,0 +1,23 @@
# frozen_string_literal: true
module ReactComponentHelper
def react_component(name, props = {}, &block)
data = { component: name.to_s.camelcase, props: Oj.dump(props) }
if block.nil?
div_tag_with_data(data)
else
content_tag(:div, data: data, &block)
end
end
def react_admin_component(name, props = {})
data = { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) }
div_tag_with_data(data)
end
private
def div_tag_with_data(data)
content_tag(:div, nil, data: data)
end
end

@ -105,94 +105,10 @@ module StatusesHelper
end end
end end
def sensitized?(status, account)
if !account.nil? && account.id == status.account_id
status.sensitive
else
status.account.sensitized? || status.sensitive
end
end
def embedded_view? def embedded_view?
params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION
end end
def render_video_component(status, **options)
video = status.ordered_media_attachments.first
meta = video.file.meta || {}
component_params = {
sensitive: sensitized?(status, current_account),
src: full_asset_url(video.file.url(:original)),
preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)),
alt: video.description,
blurhash: video.blurhash,
frameRate: meta.dig('original', 'frame_rate'),
inline: true,
media: [
ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer),
].as_json,
}.merge(**options)
react_component :video, component_params do
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
def render_audio_component(status, **options)
audio = status.ordered_media_attachments.first
meta = audio.file.meta || {}
component_params = {
src: full_asset_url(audio.file.url(:original)),
poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url),
alt: audio.description,
backgroundColor: meta.dig('colors', 'background'),
foregroundColor: meta.dig('colors', 'foreground'),
accentColor: meta.dig('colors', 'accent'),
duration: meta.dig('original', 'duration'),
}.merge(**options)
react_component :audio, component_params do
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
def render_media_gallery_component(status, **options)
component_params = {
sensitive: sensitized?(status, current_account),
autoplay: prefers_autoplay?,
media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json },
}.merge(**options)
react_component :media_gallery, component_params do
render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
def render_card_component(status, **options)
component_params = {
sensitive: sensitized?(status, current_account),
maxDescription: 160,
card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json,
}.merge(**options)
react_component :card, component_params
end
def render_poll_component(status, **options)
component_params = {
disabled: true,
poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json,
}.merge(**options)
react_component :poll, component_params do
render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? }
end
end
def prefers_autoplay? def prefers_autoplay?
ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif
end end

@ -0,0 +1,57 @@
<svg width="293" height="264" viewBox="0 0 293 264" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.1 204.9C28.1 210.7 22.3 209.9 15.9 204.3C13.4 202.1 11.3 205.2 15.3 209.2C19.3 213.2 28.8 217.5 37.3 210" fill="#E09C5C"/>
<path d="M34.1 204.9C28.1 210.7 22.3 209.9 15.9 204.3C13.4 202.1 11.3 205.2 15.3 209.2C19.3 213.2 28.8 217.5 37.3 210" stroke="#946F3A" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M197.2 1.90002H137.4C122.4 1.90002 110.1 14.2 110.1 29.2V42.1C110.1 48.4 112.3 54.2 115.9 58.9C109.2 72.6 94.5 75.8 94.5 75.8C113.4 78.1 119.1 72.3 125.2 66.6C128.9 68.4 133 69.5 137.4 69.5H197.2C212.2 69.5 224.5 57.2 224.5 42.2V29.3C224.6 14.2 212.3 1.90002 197.2 1.90002Z" fill="white" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M78.3 210.9C78.2 222.8 75.9 232 86.2 232.1C96.5 232.2 99.4 229.6 99.3 222C99.2 214.4 98.7 201 98.7 201" fill="#E09C5C"/>
<path d="M78.3 210.9C78.2 222.8 75.9 232 86.2 232.1C96.5 232.2 99.4 229.6 99.3 222C99.2 214.4 98.7 201 98.7 201" stroke="#946F3A" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M63.8 104.2C43.6 104.2 28.7 111.8 28.7 140.2C28.7 168.6 28.7 186.5 28.7 194.5C28.7 202.5 33.1 216.3 50.5 216.3C67.9 216.3 66.1 216.3 74.6 216.3C83.1 216.3 90.9 213.5 97.1 206.9C103.3 200.3 106.3 193.6 106.3 181.4C106.3 169.2 106.3 134.8 106.3 134.8C106.3 134.8 126.7 134.3 135.7 121.5C144.6 108.7 142.6 93.3001 141.4 88.9001C141.4 88.9001 146.5 88.4 145.4 84.5C144.3 80.6 138.1 81.2001 135.3 83.4001C135.3 83.4001 133.7 81.6 128.3 81.7C122.9 81.8 124.6 87.9001 129 88.4001C129 88.4001 131.4 102.2 124.4 107.1C117.4 112 103 113.7 94.6 109.8C86.1 106.1 83.3 104.2 63.8 104.2Z" fill="#FBC16C" stroke="#946F3A" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M50.5 215.2C30.4 215.2 29.9 196.7 29.9 194.6V171.5C42.9 171.6 48.7 173 48.7 183.9C48.7 197.1 50.9 203.7 62.6 203.7H90.4C91.1 203.7 91.7 203.7 92.3 203.7C92.9 203.7 93.4 203.7 94 203.7C95.7 203.7 97.4 203.6 99 203C98.2 204 97.3 205.1 96.3 206.2C90.7 212.2 83.4 215.2 74.7 215.2H50.5Z" fill="#E09C5C"/>
<path d="M56.8 110.5C47.2 107.6 42.1 105.6 45 97.2C47.9 88.8 62 90.6 69.7 94.5C69.7 94.5 73.7 92.1 76 91.2C78.3 90.3 82.6 89.9001 82.7 92.9001C82.7 92.9001 88.1 93.5001 87.1 97.8001C87.1 97.8001 93.5 101.5 79 108.5" fill="#FBC16C"/>
<path d="M56.8 110.5C47.2 107.6 42.1 105.6 45 97.2C47.9 88.8 62 90.6 69.7 94.5C69.7 94.5 73.7 92.1 76 91.2C78.3 90.3 82.6 89.9001 82.7 92.9001C82.7 92.9001 88.1 93.5001 87.1 97.8001C87.1 97.8001 93.5 101.5 79 108.5" stroke="#946F3A" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M40.5 119.9C40.5 113.5 36.7 109.6 28.7 109.6C20.7 109.6 13.9 117.1 15.7 128.3C17.5 139.5 11.4 148.2 18.5 153.2C25.6 158.2 32.9 155.1 35.2 151.3C35.2 151.3 42.8 153.3 42.2 141.4" fill="#FBC16C"/>
<path d="M40.5 119.9C40.5 113.5 36.7 109.6 28.7 109.6C20.7 109.6 13.9 117.1 15.7 128.3C17.5 139.5 11.4 148.2 18.5 153.2C25.6 158.2 32.9 155.1 35.2 151.3C35.2 151.3 42.8 153.3 42.2 141.4" stroke="#946F3A" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M69.3 134.2C63.6 134.2 60.5 138.6 61.8 144.4C63.1 150.1 66.8 154.4 73.2 154.6C79.6 154.7 85.7 152.5 87.6 143.2C89.5 133.9 86 133.8 83.2 133.8C80.4 133.8 69.3 134.2 69.3 134.2Z" fill="#544024" stroke="#3B3024" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M78.8 147.3C78.8 140.6 73.4 135.1 66.6 135.1C66 135.1 65.5001 135.2 64.9001 135.2C62.1001 136.8 60.8 140.2 61.7 144.3C63 150 66.7 154.3 73.1 154.5C74.2 154.5 75.3001 154.5 76.4001 154.3C78.0001 152.3 78.8 149.9 78.8 147.3Z" fill="#693131" stroke="#381916" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M83.1 140.9V133.7C80.1 133.7 69.3 134.1 69.3 134.1C64.8 134.1 61.9 136.9 61.5 140.9H83.1Z" fill="white" stroke="#7D7D65" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M48.2 149.1C54.2 154.9 62.5 154.9 66.3 151.7C70.1 148.5 72.1 140.3 69 139.5C65.9 138.7 66.6 144.2 63.8 145.8C61 147.4 57.8 147 54.8 145.1C51.9 143.2 48.9 141.9 47.4 143.7C46.1 145.7 46.8 147.7 48.2 149.1Z" fill="white" stroke="#7D7D65" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M251.4 179.1C245.7 185.3 237.4 185.7 233.4 182.7C229.4 179.7 227 171.7 230 170.7C233 169.7 232.7 175.2 235.5 176.7C238.3 178.2 241.6 177.6 244.4 175.6C247.2 173.6 250.1 172 251.7 173.9C253.3 175.8 252.8 177.7 251.4 179.1Z" stroke="black" stroke-width="0.5707" stroke-miterlimit="10"/>
<path d="M36.3 153.7L15.8 153.8C15.8 153.8 15.1 150.4 12 150.9C8.90001 151.4 8.19998 153.2 8.09998 154.3C8.09998 154.3 1.30002 154.7 1.20002 161.5C1.10002 168.3 6.1 171 13.3 170.6C20.5 170.2 37.7 170.6 37.7 170.6" fill="#FBC16C"/>
<path d="M36.3 153.7L15.8 153.8C15.8 153.8 15.1 150.4 12 150.9C8.90001 151.4 8.19998 153.2 8.09998 154.3C8.09998 154.3 1.30002 154.7 1.20002 161.5C1.10002 168.3 6.1 171 13.3 170.6C20.5 170.2 37.7 170.6 37.7 170.6" stroke="#946F3A" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M37.4 210.9C37.3 222.8 35 232 45.3 232.1C55.6 232.2 58.5 229.6 58.4 222C58.3 214.4 58.2 211.6 58.2 211.6" fill="#E09C5C"/>
<path d="M37.4 210.9C37.3 222.8 35 232 45.3 232.1C55.6 232.2 58.5 229.6 58.4 222C58.3 214.4 58.2 211.6 58.2 211.6" stroke="#946F3A" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M54.7 138.2C53.8 138.2 53.1 137.5 53.1 136.6V125.7C53.1 124.8 53.8 124.1 54.7 124.1C55.6 124.1 56.3 124.8 56.3 125.7V136.6C56.3 137.5 55.6 138.2 54.7 138.2Z" fill="#402F19"/>
<path d="M196.8 191.4C189.6 192.3 180.1 190.7 175.2 189.6C170.3 188.5 167.4 193.9 166.7 199.1C166 204.3 178.4 213.2 199.4 207.8" fill="#FBE6C6"/>
<path d="M196.8 191.4C189.6 192.3 180.1 190.7 175.2 189.6C170.3 188.5 167.4 193.9 166.7 199.1C166 204.3 178.4 213.2 199.4 207.8" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M265 188.6C278.1 192.7 287.2 200.3 287 208.9C286.8 217.5 277.4 223.1 270.5 224.4C263.6 225.7 260.7 218.3 261.6 213.3C262.5 208.3 265 206.7 265 206.7" fill="#FBE6C6"/>
<path d="M265 188.6C278.1 192.7 287.2 200.3 287 208.9C286.8 217.5 277.4 223.1 270.5 224.4C263.6 225.7 260.7 218.3 261.6 213.3C262.5 208.3 265 206.7 265 206.7" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M260 204C265 206.6 270.1 209.4 270.1 209.4Z" fill="#FBE6C6"/>
<path d="M260 204C265 206.6 270.1 209.4 270.1 209.4" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M266 150.4C266 144 269.8 140.1 277.8 140.1C285.8 140.1 292.6 147.6 290.8 158.8C289 170 295.1 178.7 288 183.7C280.9 188.6 273.6 185.6 271.3 181.8C271.3 181.8 263.7 183.8 264.3 171.9" fill="#FBE6C6"/>
<path d="M266 150.4C266 144 269.8 140.1 277.8 140.1C285.8 140.1 292.6 147.6 290.8 158.8C289 170 295.1 178.7 288 183.7C280.9 188.6 273.6 185.6 271.3 181.8C271.3 181.8 263.7 183.8 264.3 171.9" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M230.1 133.7C200.4 133.4 195.7 141.6 188.1 148.1C180.5 154.7 170.5 166.9 151.5 167.6C151.5 167.6 149 164.1 146.3 166.6C143.6 169.1 144.1 172.5 146 173.8C146 173.8 142.3 177.5 146.2 181.4C150.1 185.3 153.4 181.4 153.4 181.4C153.4 181.4 167.8 182.7 179.3 177.4C190.7 172 194.4 174.1 194.4 174.1C194.4 174.1 194.4 208.7 194.4 224.5C194.4 240.3 201.6 246.5 220.3 246.5C239 246.5 257.1 248.2 264.8 240.3C272.5 232.4 271.2 221.1 270.7 211.1C270.2 201.1 270.7 183 270.7 167.6C270.7 152.2 269.3 134 230.1 133.7Z" fill="#FBE6C6" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M242.3 244C242.3 248.8 241.8 250.6 242.1 254.1C240.5 255.2 239.9 257.1 240.4 258.7C241 260.7 244 262.7 250.3 262.6C260.7 262.7 263.5 260.1 263.4 252.5C263.3 249 263.2 244.3 263.1 240.3" fill="#DCCEB5"/>
<path d="M242.3 244C242.3 248.8 241.8 250.6 242.1 254.1C240.5 255.2 239.9 257.1 240.4 258.7C241 260.7 244 262.7 250.3 262.6C260.7 262.7 263.5 260.1 263.4 252.5C263.3 249 263.2 244.3 263.1 240.3" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M236.5 245.4C233.9 245.4 231.4 245.4 228.6 245.3C225.8 245.3 223 245.2 220.2 245.2C202.2 245.2 195.5 239.5 195.5 224.3C195.5 221.8 199.8 219.5 204.5 219.5C206.8 219.5 211.1 220.1 213.7 223.8C217.5 229 222.6 230.9 230 230.9C234.5 230.9 244.5 231.1 247.9 228.5C252.4 225.2 253.7 220.6 261.2 218.9C264.2 218.2 266.8 217.7 269.8 218.5C270 227.8 268.5 235.7 263.9 239.3C258.4 243.7 249.4 245.4 236.5 245.4Z" fill="#DCCEB5"/>
<path d="M243.9 141.2C250.9 141.1 254.6 136.8 254.1 132.6C253.5 128.5 250.6 122.7 237.5 123.4C231.7 123.7 228.8 127.5 226 127.5C223.2 127.4 217.7 119.4 212.9 119.9C208.1 120.4 207.8 123.8 208.6 125.4C208.6 125.4 204.3 126.7 206.7 132.3C209.1 137.9 214 139.8 218.8 140.4" fill="#FBE6C6"/>
<path d="M243.9 141.2C250.9 141.1 254.6 136.8 254.1 132.6C253.5 128.5 250.6 122.7 237.5 123.4C231.7 123.7 228.8 127.5 226 127.5C223.2 127.4 217.7 119.4 212.9 119.9C208.1 120.4 207.8 123.8 208.6 125.4C208.6 125.4 204.3 126.7 206.7 132.3C209.1 137.9 214 139.8 218.8 140.4" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M203.7 241.5C203.7 246.3 203.2 250.7 203.5 254.2C201.9 255.3 201.3 257.2 201.8 258.8C202.4 260.8 205.4 262.8 211.7 262.7C222.1 262.8 224.9 260.2 224.8 252.6C224.8 249.7 224.7 248.9 224.6 245.4" fill="#DCCEB5"/>
<path d="M203.7 241.5C203.7 246.3 203.2 250.7 203.5 254.2C201.9 255.3 201.3 257.2 201.8 258.8C202.4 260.8 205.4 262.8 211.7 262.7C222.1 262.8 224.9 260.2 224.8 252.6C224.8 249.7 224.7 248.9 224.6 245.4" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M194.8 22.9H140.2C138.3 22.9 136.8 21.4 136.8 19.5C136.8 17.6 138.3 16.1 140.2 16.1H194.8C196.7 16.1 198.2 17.6 198.2 19.5C198.2 21.4 196.7 22.9 194.8 22.9Z" fill="#4A4439"/>
<path d="M194.8 39.8H140.2C138.3 39.8 136.8 38.3 136.8 36.4C136.8 34.5 138.3 33 140.2 33H194.8C196.7 33 198.2 34.5 198.2 36.4C198.2 38.2 196.7 39.8 194.8 39.8Z" fill="#4A4439"/>
<path d="M194.8 56.6H140.2C138.3 56.6 136.8 55.1 136.8 53.2C136.8 51.3 138.3 49.8 140.2 49.8H194.8C196.7 49.8 198.2 51.3 198.2 53.2C198.2 55.1 196.7 56.6 194.8 56.6Z" fill="#4A4439"/>
<path d="M205.9 150.4C205.9 144 209.7 140.1 217.7 140.1C225.7 140.1 232.5 147.6 230.7 158.8C228.9 170 235 178.7 227.9 183.7C220.8 188.6 213.5 185.6 211.2 181.8C211.2 181.8 203.6 183.8 204.2 171.9" fill="#FBE6C6"/>
<path d="M205.9 150.4C205.9 144 209.7 140.1 217.7 140.1C225.7 140.1 232.5 147.6 230.7 158.8C228.9 170 235 178.7 227.9 183.7C220.8 188.6 213.5 185.6 211.2 181.8C211.2 181.8 203.6 183.8 204.2 171.9" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M104.3 166C115.9 165.4 125.9 161.4 131 156.7C136.8 151.4 139.9 146.2 134.7 141.6C129.6 137.1 124.6 141.5 124.6 141.5C124.6 141.5 122.6 138.4 119.6 140.7C116.6 143 118.8 145.2 118.8 145.2C118.8 145.2 115.2 150.2 103.9 150.7" fill="#FBC16C"/>
<path d="M104.3 166C115.9 165.4 125.9 161.4 131 156.7C136.8 151.4 139.9 146.2 134.7 141.6C129.6 137.1 124.6 141.5 124.6 141.5C124.6 141.5 122.6 138.4 119.6 140.7C116.6 143 118.8 145.2 118.8 145.2C118.8 145.2 115.2 150.2 103.9 150.7" stroke="#946F3A" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M49 139L45.5 139.6C44.5 139.8 43.5001 139.1 43.4001 138.1C43.2001 137.1 43.9001 136.1 44.9001 136L48.4001 135.4C49.4001 135.2 50.4 135.9 50.5 136.9C50.7 137.9 50.1 138.9 49 139Z" fill="#E68A4C"/>
<path d="M119.7 114.2C120.4 114 120.7 113.8 121.3 113.5" stroke="#FBD7A3" stroke-width="2.8533" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M114.5 115.6C115.4 115.4 114.9 115.6 115.7 115.4" stroke="#FBD7A3" stroke-width="2.8533" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M102.2 115.8C104.4 116.1 106.6 116.2 108.8 116.2" stroke="#FBD7A3" stroke-width="2.8533" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M88.1 111.8C90.5 112.9 93.1 113.8 95.8 114.5" stroke="#FBD7A3" stroke-width="2.8533" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M207.1 140.8C207.8 140.5 208.4 140.3 209 140" stroke="white" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M201.6 143.5C202.5 143 202.5 142.9 203.4 142.4" stroke="white" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M193.8 148.9C195.4 147.7 196.3 147 197.9 146" stroke="white" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M186.1 156.2C187.3 154.9 188.7 153.6 190.1 152.3" stroke="white" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M262.1 235.4C268.1 241.2 273.9 240.4 280.3 234.8C282.8 232.6 284.9 235.7 280.9 239.7C276.9 243.7 267.4 248 258.9 240.5" fill="#DCCEB5"/>
<path d="M262.1 235.4C268.1 241.2 273.9 240.4 280.3 234.8C282.8 232.6 284.9 235.7 280.9 239.7C276.9 243.7 267.4 248 258.9 240.5" stroke="#668794" stroke-width="2.2827" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

@ -74,6 +74,7 @@ export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTIO
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS'; export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
const messages = defineMessages({ const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
@ -125,6 +126,15 @@ export function resetCompose() {
}; };
} }
export const focusCompose = (routerHistory, defaultText) => dispatch => {
dispatch({
type: COMPOSE_FOCUS,
defaultText,
});
ensureComposeIsVisible(routerHistory);
};
export function mentionCompose(account, routerHistory) { export function mentionCompose(account, routerHistory) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({

@ -135,8 +135,7 @@ export const showSearch = () => ({
type: SEARCH_SHOW, type: SEARCH_SHOW,
}); });
export const openURL = routerHistory => (dispatch, getState) => { export const openURL = (value, history, onFailure) => (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const signedIn = !!getState().getIn(['meta', 'me']); const signedIn = !!getState().getIn(['meta', 'me']);
if (!signedIn) { if (!signedIn) {
@ -148,15 +147,21 @@ export const openURL = routerHistory => (dispatch, getState) => {
api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => { api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
if (response.data.accounts?.length > 0) { if (response.data.accounts?.length > 0) {
dispatch(importFetchedAccounts(response.data.accounts)); dispatch(importFetchedAccounts(response.data.accounts));
routerHistory.push(`/@${response.data.accounts[0].acct}`); history.push(`/@${response.data.accounts[0].acct}`);
} else if (response.data.statuses?.length > 0) { } else if (response.data.statuses?.length > 0) {
dispatch(importFetchedStatuses(response.data.statuses)); dispatch(importFetchedStatuses(response.data.statuses));
routerHistory.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`); history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
} else if (onFailure) {
onFailure();
} }
dispatch(fetchSearchSuccess(response.data, value)); dispatch(fetchSearchSuccess(response.data, value));
}).catch(err => { }).catch(err => {
dispatch(fetchSearchFail(err)); dispatch(fetchSearchFail(err));
if (onFailure) {
onFailure();
}
}); });
}; };

@ -12,8 +12,8 @@ import Skeleton from 'mastodon/components/skeleton';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { counterRenderer } from 'mastodon/components/common_counter'; import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number'; import ShortNumber from 'mastodon/components/short_number';
import Icon from 'mastodon/components/icon';
import classNames from 'classnames'; import classNames from 'classnames';
import VerifiedBadge from 'mastodon/components/verified_badge';
const messages = defineMessages({ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -27,26 +27,6 @@ const messages = defineMessages({
block: { id: 'account.block', defaultMessage: 'Block @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' },
}); });
class VerifiedBadge extends React.PureComponent {
static propTypes = {
link: PropTypes.string.isRequired,
verifiedAt: PropTypes.string.isRequired,
};
render () {
const { link } = this.props;
return (
<span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' />
<span dangerouslySetInnerHTML={{ __html: link }} />
</span>
);
}
}
class Account extends ImmutablePureComponent { class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
const Check = () => ( const Check = () => (
<svg width='14' height='11' viewBox='0 0 14 11'> <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='currentColor'>
<path d='M11.264 0L5.26 6.004 2.103 2.847 0 4.95l5.26 5.26 8.108-8.107L11.264 0' fill='currentColor' fillRule='evenodd' /> <path fillRule='evenodd' d='M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z' clipRule='evenodd' />
</svg> </svg>
); );

@ -12,13 +12,19 @@ export default class ColumnBackButton extends React.PureComponent {
static propTypes = { static propTypes = {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
onClick: PropTypes.func,
}; };
handleClick = () => { handleClick = () => {
if (window.history && window.history.state) { const { router } = this.context;
this.context.router.history.goBack(); const { onClick } = this.props;
if (onClick) {
onClick();
} else if (window.history && window.history.state) {
router.history.goBack();
} else { } else {
this.context.router.history.push('/'); router.history.push('/');
} }
}; };

@ -32,7 +32,7 @@ class EditedTimestamp extends React.PureComponent {
renderHeader = items => { renderHeader = items => {
return ( return (
<FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {{count} time} other {{count} times}}' values={{ count: items.size - 1 }} /> <FormattedMessage id='status.edited_x_times' defaultMessage='Edited {count, plural, one {# time} other {# times}}' values={{ count: items.size - 1 }} />
); );
}; };

@ -43,7 +43,7 @@ class SilentErrorBoundary extends React.Component {
export const accountsCountRenderer = (displayNumber, pluralReady) => ( export const accountsCountRenderer = (displayNumber, pluralReady) => (
<FormattedMessage <FormattedMessage
id='trends.counter_by_accounts' id='trends.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}' defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
values={{ values={{
count: pluralReady, count: pluralReady,
counter: <strong>{displayNumber}</strong>, counter: <strong>{displayNumber}</strong>,

@ -1,10 +1,15 @@
import React from 'react'; import React from 'react';
import logo from 'mastodon/../images/logo.svg';
const Logo = () => ( export const WordmarkLogo = () => (
<svg viewBox='0 0 261 66' className='logo' role='img'> <svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
<title>Mastodon</title> <title>Mastodon</title>
<use xlinkHref='#logo-symbol-wordmark' /> <use xlinkHref='#logo-symbol-wordmark' />
</svg> </svg>
); );
export default Logo; export const SymbolLogo = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' />
);
export default WordmarkLogo;

@ -32,17 +32,14 @@ function ShortNumber({ value, renderer, children }) {
const shortNumber = toShortNumber(value); const shortNumber = toShortNumber(value);
const [, division] = shortNumber; const [, division] = shortNumber;
// eslint-disable-next-line eqeqeq
if (children != null && renderer != null) { if (children != null && renderer != null) {
console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.'); console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
} }
// eslint-disable-next-line eqeqeq
const customRenderer = children != null ? children : renderer; const customRenderer = children != null ? children : renderer;
const displayNumber = <ShortNumberCounter value={shortNumber} />; const displayNumber = <ShortNumberCounter value={shortNumber} />;
// eslint-disable-next-line eqeqeq
return customRenderer != null return customRenderer != null
? customRenderer(displayNumber, pluralReady(value, division)) ? customRenderer(displayNumber, pluralReady(value, division))
: displayNumber; : displayNumber;

@ -68,6 +68,9 @@ class Status extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
previousId: PropTypes.string,
nextInReplyToId: PropTypes.string,
rootId: PropTypes.string,
onClick: PropTypes.func, onClick: PropTypes.func,
onReply: PropTypes.func, onReply: PropTypes.func,
onFavourite: PropTypes.func, onFavourite: PropTypes.func,
@ -309,10 +312,7 @@ class Status extends ImmutablePureComponent {
}; };
render () { render () {
let media = null; const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId } = this.props;
let statusAvatar, prepend, rebloggedByText;
const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture } = this.props;
let { status, account, ...other } = this.props; let { status, account, ...other } = this.props;
@ -334,6 +334,8 @@ class Status extends ImmutablePureComponent {
openMedia: this.handleHotkeyOpenMedia, openMedia: this.handleHotkeyOpenMedia,
}; };
let media, statusAvatar, prepend, rebloggedByText;
if (hidden) { if (hidden) {
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
@ -345,7 +347,11 @@ class Status extends ImmutablePureComponent {
); );
} }
const connectUp = previousId && previousId === status.get('in_reply_to_id');
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
const matchedFilters = status.get('matched_filters'); const matchedFilters = status.get('matched_filters');
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) { if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : { const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp, moveUp: this.handleHotkeyMoveUp,
@ -519,7 +525,10 @@ class Status extends ImmutablePureComponent {
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}> <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}> <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
<div onClick={this.handleClick} className='status__info'> <div onClick={this.handleClick} className='status__info'>
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'> <a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span> <span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>

@ -104,7 +104,7 @@ class StatusContent extends React.PureComponent {
link.setAttribute('href', `/@${mention.get('acct')}`); link.setAttribute('href', `/@${mention.get('acct')}`);
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
link.setAttribute('href', `/tags/${link.text.slice(1)}`); link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
} else { } else {
link.setAttribute('title', link.href); link.setAttribute('title', link.href);
link.classList.add('unhandled-link'); link.classList.add('unhandled-link');

@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'mastodon/components/icon';
class VerifiedBadge extends React.PureComponent {
static propTypes = {
link: PropTypes.string.isRequired,
verifiedAt: PropTypes.string.isRequired,
};
render () {
const { link } = this.props;
return (
<span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' />
<span dangerouslySetInnerHTML={{ __html: link }} />
</span>
);
}
}
export default VerifiedBadge;

@ -67,6 +67,7 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
status: getStatus(state, props), status: getStatus(state, props),
nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null,
pictureInPicture: getPictureInPicture(state, props), pictureInPicture: getPictureInPicture(state, props),
}); });

@ -80,6 +80,7 @@ class Header extends ImmutablePureComponent {
static contextTypes = { static contextTypes = {
identity: PropTypes.object, identity: PropTypes.object,
router: PropTypes.object,
}; };
static propTypes = { static propTypes = {
@ -101,11 +102,16 @@ class Header extends ImmutablePureComponent {
onChangeLanguages: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired, onOpenAvatar: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
}; };
setRef = c => {
this.node = c;
};
openEditProfile = () => { openEditProfile = () => {
window.open('/settings/profile', '_blank'); window.open('/settings/profile', '_blank');
}; };
@ -162,6 +168,61 @@ class Header extends ImmutablePureComponent {
}); });
}; };
handleHashtagClick = e => {
const { router } = this.context;
const value = e.currentTarget.textContent.replace(/^#/, '');
if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
router.history.push(`/tags/${value}`);
}
};
handleMentionClick = e => {
const { router } = this.context;
const { onOpenURL } = this.props;
if (router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
const link = e.currentTarget;
onOpenURL(link.href, router.history, () => {
window.location = link.href;
});
}
};
_attachLinkEvents () {
const node = this.node;
if (!node) {
return;
}
const links = node.querySelectorAll('a');
let link;
for (var i = 0; i < links.length; ++i) {
link = links[i];
if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.handleHashtagClick, false);
} else if (link.classList.contains('mention')) {
link.addEventListener('click', this.handleMentionClick, false);
}
}
}
componentDidMount () {
this._attachLinkEvents();
}
componentDidUpdate () {
this._attachLinkEvents();
}
render () { render () {
const { account, hidden, intl, domain } = this.props; const { account, hidden, intl, domain } = this.props;
const { signedIn, permissions } = this.context.identity; const { signedIn, permissions } = this.context.identity;
@ -360,7 +421,7 @@ class Header extends ImmutablePureComponent {
{!(suspended || hidden) && ( {!(suspended || hidden) && (
<div className='account__header__extra'> <div className='account__header__extra'>
<div className='account__header__bio'> <div className='account__header__bio' ref={this.setRef}>
{(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />} {(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />}
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />} {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />}

@ -26,6 +26,7 @@ export default class Header extends ImmutablePureComponent {
onChangeLanguages: PropTypes.func.isRequired, onChangeLanguages: PropTypes.func.isRequired,
onInteractionModal: PropTypes.func.isRequired, onInteractionModal: PropTypes.func.isRequired,
onOpenAvatar: PropTypes.func.isRequired, onOpenAvatar: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
hideTabs: PropTypes.bool, hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
@ -137,6 +138,7 @@ export default class Header extends ImmutablePureComponent {
onChangeLanguages={this.handleChangeLanguages} onChangeLanguages={this.handleChangeLanguages}
onInteractionModal={this.handleInteractionModal} onInteractionModal={this.handleInteractionModal}
onOpenAvatar={this.handleOpenAvatar} onOpenAvatar={this.handleOpenAvatar}
onOpenURL={this.props.onOpenURL}
domain={this.props.domain} domain={this.props.domain}
hidden={hidden} hidden={hidden}
/> />

@ -10,6 +10,7 @@ import {
pinAccount, pinAccount,
unpinAccount, unpinAccount,
} from '../../../actions/accounts'; } from '../../../actions/accounts';
import { openURL } from 'mastodon/actions/search';
import { import {
mentionCompose, mentionCompose,
directCompose, directCompose,
@ -159,6 +160,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
})); }));
}, },
onOpenURL (url, routerHistory, onFailure) {
dispatch(openURL(url, routerHistory, onFailure));
},
}); });
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));

@ -20,6 +20,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz'; import { length } from 'stringz';
import { countableText } from '../util/counter'; import { countableText } from '../util/counter';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import classNames from 'classnames';
import { maxChars } from '../../../initial_state'; import { maxChars } from '../../../initial_state';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
@ -71,6 +72,10 @@ class ComposeForm extends ImmutablePureComponent {
autoFocus: false, autoFocus: false,
}; };
state = {
highlighted: false,
};
handleChange = (e) => { handleChange = (e) => {
this.props.onChange(e.target.value); this.props.onChange(e.target.value);
}; };
@ -144,6 +149,10 @@ class ComposeForm extends ImmutablePureComponent {
this._updateFocusAndSelection({ }); this._updateFocusAndSelection({ });
} }
componentWillUnmount () {
if (this.timeout) clearTimeout(this.timeout);
}
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
this._updateFocusAndSelection(prevProps); this._updateFocusAndSelection(prevProps);
} }
@ -174,6 +183,8 @@ class ComposeForm extends ImmutablePureComponent {
Promise.resolve().then(() => { Promise.resolve().then(() => {
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd); this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
this.autosuggestTextarea.textarea.focus(); this.autosuggestTextarea.textarea.focus();
this.setState({ highlighted: true });
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
}).catch(console.error); }).catch(console.error);
} else if(prevProps.isSubmitting && !this.props.isSubmitting) { } else if(prevProps.isSubmitting && !this.props.isSubmitting) {
this.autosuggestTextarea.textarea.focus(); this.autosuggestTextarea.textarea.focus();
@ -208,6 +219,7 @@ class ComposeForm extends ImmutablePureComponent {
render () { render () {
const { intl, onPaste, autoFocus } = this.props; const { intl, onPaste, autoFocus } = this.props;
const { highlighted } = this.state;
const disabled = this.props.isSubmitting; const disabled = this.props.isSubmitting;
let publishText = ''; let publishText = '';
@ -246,41 +258,43 @@ class ComposeForm extends ImmutablePureComponent {
/> />
</div> </div>
<AutosuggestTextarea <div className={classNames('compose-form__highlightable', { active: highlighted })}>
ref={this.setAutosuggestTextarea} <AutosuggestTextarea
placeholder={intl.formatMessage(messages.placeholder)} ref={this.setAutosuggestTextarea}
disabled={disabled} placeholder={intl.formatMessage(messages.placeholder)}
value={this.props.text} disabled={disabled}
onChange={this.handleChange} value={this.props.text}
suggestions={this.props.suggestions} onChange={this.handleChange}
onFocus={this.handleFocus} suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown} onFocus={this.handleFocus}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onKeyDown={this.handleKeyDown}
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionSelected={this.onSuggestionSelected} onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onPaste={onPaste} onSuggestionSelected={this.onSuggestionSelected}
autoFocus={autoFocus} onPaste={onPaste}
lang={this.props.lang} autoFocus={autoFocus}
> lang={this.props.lang}
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> >
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<div className='compose-form__modifiers'>
<UploadFormContainer /> <div className='compose-form__modifiers'>
<PollFormContainer /> <UploadFormContainer />
</div> <PollFormContainer />
</AutosuggestTextarea> </div>
</AutosuggestTextarea>
<div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'> <div className='compose-form__buttons-wrapper'>
<UploadButtonContainer /> <div className='compose-form__buttons'>
<PollButtonContainer /> <UploadButtonContainer />
<PrivacyDropdownContainer disabled={this.props.isEditing} /> <PollButtonContainer />
<SpoilerButtonContainer /> <PrivacyDropdownContainer disabled={this.props.isEditing} />
<LanguageDropdown /> <SpoilerButtonContainer />
</div> <LanguageDropdown />
</div>
<div className='character-counter__wrapper'>
<CharacterCounter max={maxChars} text={this.getFulltextForCharacterCounting()} /> <div className='character-counter__wrapper'>
<CharacterCounter max={maxChars} text={this.getFulltextForCharacterCounting()} />
</div>
</div> </div>
</div> </div>

@ -161,9 +161,9 @@ class Search extends React.PureComponent {
handleURLClick = () => { handleURLClick = () => {
const { router } = this.context; const { router } = this.context;
const { onOpenURL } = this.props; const { value, onOpenURL } = this.props;
onOpenURL(router.history); onOpenURL(value, router.history);
}; };
handleStatusSearch = () => { handleStatusSearch = () => {

@ -126,7 +126,7 @@ class SearchResults extends ImmutablePureComponent {
<div className='search-results'> <div className='search-results'>
<div className='search-results__header'> <div className='search-results__header'>
<Icon id='search' fixedWidth /> <Icon id='search' fixedWidth />
<FormattedMessage id='search_results.total' defaultMessage='{count, number} {count, plural, one {result} other {results}}' values={{ count }} /> <FormattedMessage id='search_results.total' defaultMessage='{count, plural, one {# result} other {# results}}' values={{ count }} />
</div> </div>
{accounts} {accounts}

@ -34,8 +34,8 @@ const mapDispatchToProps = dispatch => ({
dispatch(showSearch()); dispatch(showSearch());
}, },
onOpenURL (routerHistory) { onOpenURL (q, routerHistory) {
dispatch(openURL(routerHistory)); dispatch(openURL(q, routerHistory));
}, },
onClickSearchResult (q, type) { onClickSearchResult (q, type) {

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

Loading…
Cancel
Save