Merge remote-tracking branch 'upstream/main' into develop

main
Jeremy Kescher 1 year ago
commit 9eb149477a
No known key found for this signature in database
GPG Key ID: 80A419A7A613DFA4

@ -27,6 +27,7 @@ module.exports = {
'import',
'promise',
'@typescript-eslint',
'formatjs',
],
parserOptions: {
@ -71,7 +72,7 @@ module.exports = {
'comma-style': ['warn', 'last'],
'consistent-return': 'error',
'dot-notation': 'error',
eqeqeq: 'error',
eqeqeq: ['error', 'always', { 'null': 'ignore' }],
indent: ['warn', 2],
'jsx-quotes': ['error', 'prefer-single'],
'no-case-declarations': 'off',
@ -218,6 +219,25 @@ module.exports = {
'promise/no-callback-in-promise': 'off',
'promise/no-nesting': '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: [

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

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

@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.48.1.
# using RuboCop version 1.50.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@ -132,7 +132,6 @@ Lint/DuplicateBranch:
Lint/EmptyBlock:
Exclude:
- 'spec/controllers/api/v2/search_controller_spec.rb'
- 'spec/controllers/application_controller_spec.rb'
- 'spec/fabricators/access_token_fabricator.rb'
- 'spec/fabricators/conversation_fabricator.rb'
- 'spec/fabricators/system_key_fabricator.rb'
@ -174,11 +173,6 @@ Lint/EmptyClass:
Exclude:
- 'spec/controllers/api/base_controller_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/NonDeterministicRequireOrder:
Exclude:
- 'spec/rails_helper.rb'
Lint/NonLocalExitFromIterator:
Exclude:
- 'app/helpers/jsonld_helper.rb'
@ -251,7 +245,6 @@ Metrics/ModuleLength:
- 'app/controllers/concerns/signature_verification.rb'
- 'app/helpers/application_helper.rb'
- 'app/helpers/jsonld_helper.rb'
- 'app/helpers/statuses_helper.rb'
- 'app/models/concerns/account_interactions.rb'
- 'app/models/concerns/has_user_settings.rb'
@ -370,6 +363,7 @@ Performance/MethodObjectAsBlock:
- 'spec/models/export_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowRegexpMatch.
Performance/RedundantEqualityComparisonBlock:
Exclude:
- 'spec/requests/link_headers_spec.rb'
@ -699,7 +693,6 @@ RSpec/HookArgument:
RSpec/InstanceVariable:
Exclude:
- 'spec/controllers/api/v1/streaming_controller_spec.rb'
- 'spec/controllers/application_controller_spec.rb'
- 'spec/controllers/auth/confirmations_controller_spec.rb'
- 'spec/controllers/auth/passwords_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/oauth/authorized_applications_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/vacuum/preview_cards_vacuum_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/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:
Exclude:
- '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/domain_allows_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/features/log_in_spec.rb'
- 'spec/lib/activitypub/activity/undo_spec.rb'
@ -1225,9 +1193,6 @@ Rails/ActiveRecordCallbacksOrder:
Rails/ApplicationController:
Exclude:
- '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.
# SupportedDatabases: mysql, postgresql
@ -1405,14 +1370,6 @@ Rails/HasManyOrHasOneDependent:
- 'app/models/user.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).
# Configuration parameters: Include.
# Include: spec/**/*, test/**/*
@ -1502,15 +1459,6 @@ Rails/RakeEnvironment:
- 'lib/tasks/repo.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.
# Include: db/**/*.rb
Rails/ReversibleMigration:
@ -2256,16 +2204,11 @@ Style/MapToHash:
# SupportedStyles: literals, strict
Style/MutableConstant:
Exclude:
- 'app/lib/link_details_extractor.rb'
- 'app/models/account.rb'
- 'app/models/custom_emoji.rb'
- 'app/models/tag.rb'
- 'app/services/account_search_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'
- 'lib/mastodon/snowflake.rb'
- 'lib/mastodon/migration_warning.rb'
- 'spec/controllers/api/base_controller_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
@ -2273,12 +2216,6 @@ Style/NilLambda:
Exclude:
- '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.
# AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter:
@ -2388,7 +2325,6 @@ Style/Semicolon:
Exclude:
- 'spec/services/activitypub/process_status_update_service_spec.rb'
- 'spec/validators/blacklisted_email_validator_spec.rb'
- 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.

@ -1,7 +1,7 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '>= 2.7.0', '< 3.3.0'
ruby '>= 3.0.0'
gem 'pkg-config', '~> 1.5'
@ -9,10 +9,10 @@ gem 'puma', '~> 6.2'
gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.6'
gem 'rack', '~> 2.2.7'
gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.4'
gem 'pg', '~> 1.5'
gem 'makara', '~> 0.5'
gem 'pghero'
gem 'dotenv-rails', '~> 2.8'
@ -30,7 +30,10 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3'
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
gem 'devise_pam_authenticatable2', '~> 9.2'
@ -76,7 +79,7 @@ gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.1'
gem 'ruby-progressbar', '~> 1.11'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 6.0'
gem 'scenic', '~> 1.7'
gem 'sidekiq', '~> 6.5'
@ -121,7 +124,7 @@ group :test do
gem 'capybara', '~> 3.39'
gem 'climate_control'
gem 'faker', '~> 3.2'
gem 'json-schema', '~> 3.0'
gem 'json-schema', '~> 4.0'
gem 'rack-test', '~> 2.1'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec_junit_formatter', '~> 0.6'

@ -27,6 +27,18 @@ GIT
rails-settings-cached (0.6.6)
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
remote: https://rubygems.org/
specs:
@ -104,12 +116,12 @@ GEM
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_encrypted (3.1.0)
attr_encrypted (4.0.0)
encryptor (~> 3.0.0)
attr_required (1.0.1)
awrence (1.2.1)
aws-eventstream (1.2.0)
aws-partitions (1.743.0)
aws-partitions (1.752.0)
aws-sdk-core (3.171.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0)
@ -118,7 +130,7 @@ GEM
aws-sdk-kms (1.63.0)
aws-sdk-core (~> 3, >= 3.165.0)
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-kms (~> 1)
aws-sigv4 (~> 1.4)
@ -142,7 +154,7 @@ GEM
blurhash (0.1.7)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.0)
brakeman (5.4.1)
browser (5.3.1)
brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5)
@ -179,7 +191,7 @@ GEM
activesupport
cbor (0.5.9.6)
charlock_holmes (0.7.7)
chewy (7.3.0)
chewy (7.3.2)
activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl
@ -189,7 +201,7 @@ GEM
coderay (1.1.3)
color_diff (0.1)
concurrent-ruby (1.2.2)
connection_pool (2.3.0)
connection_pool (2.4.0)
cose (1.3.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -206,12 +218,6 @@ GEM
railties (>= 4.1.0)
responders
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 (>= 4.0.0)
rpam2 (~> 4.0)
@ -241,7 +247,7 @@ GEM
erubi (1.12.0)
et-orbi (1.2.7)
tzinfo
excon (0.97.1)
excon (0.99.0)
fabrication (2.30.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
@ -364,7 +370,7 @@ GEM
json-ld-preloaded (3.2.2)
json-ld (~> 3.2)
rdf (~> 3.2)
json-schema (3.0.0)
json-schema (4.0.0)
addressable (>= 2.8)
jsonapi-renderer (0.2.2)
jwt (2.7.0)
@ -416,11 +422,11 @@ GEM
method_source (1.0.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105)
mime-types-data (3.2023.0218.1)
mini_mime (1.1.2)
mini_portile2 (2.8.1)
minitest (5.18.0)
msgpack (1.6.0)
msgpack (1.7.0)
multi_json (1.15.0)
multipart-post (2.3.0)
net-http (0.3.2)
@ -437,7 +443,7 @@ GEM
net-ssh (>= 2.6.5, < 8.0.0)
net-smtp (0.3.3)
net-protocol
net-ssh (7.0.1)
net-ssh (7.1.0)
nio4r (2.5.9)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
@ -480,18 +486,18 @@ GEM
openssl (> 2.0)
orm_adapter (0.5.0)
ox (2.14.16)
parallel (1.22.1)
parser (3.2.2.0)
parallel (1.23.0)
parser (3.2.2.1)
ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
pg (1.4.6)
pghero (3.3.2)
pg (1.5.2)
pghero (3.3.3)
activerecord (>= 6)
pkg-config (1.5.1)
posix-spawn (0.3.15)
premailer (1.18.0)
premailer (1.21.0)
addressable
css_parser (>= 1.12.0)
htmlentities (>= 4.0.0)
@ -501,13 +507,13 @@ GEM
premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0)
public_suffix (5.0.1)
puma (6.2.1)
puma (6.2.2)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.6.2)
rack (2.2.6.4)
rack (2.2.7)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-cors (2.0.1)
@ -567,7 +573,7 @@ GEM
redis (>= 4)
redlock (1.3.2)
redis (>= 3.0.0, < 6.0)
regexp_parser (2.7.0)
regexp_parser (2.8.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.0)
@ -580,12 +586,12 @@ GEM
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.12.1)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.3)
rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-rails (6.0.1)
@ -603,7 +609,7 @@ GEM
rspec_chunked (0.6)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.49.0)
rubocop (1.50.2)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.2.0.0)
@ -615,7 +621,7 @@ GEM
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.0)
parser (>= 3.2.1.0)
rubocop-capybara (2.17.1)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-performance (1.17.1)
rubocop (>= 1.7.0, < 2.0)
@ -771,6 +777,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10)
addressable (~> 2.8)
annotate (~> 3.2)
attr_encrypted (~> 4.0)
aws-sdk-s3 (~> 1.120)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
@ -792,7 +799,7 @@ DEPENDENCIES
concurrent-ruby
connection_pool
devise (~> 4.9)
devise-two-factor (~> 4.0)
devise-two-factor!
devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.2)
doorkeeper (~> 5.6)
@ -817,7 +824,7 @@ DEPENDENCIES
idn-ruby
json-ld
json-ld-preloaded (~> 3.2)
json-schema (~> 3.0)
json-schema (~> 4.0)
kaminari (~> 1.2)
kt-paperclip (~> 7.1)!
letter_opener (~> 1.8)
@ -840,7 +847,7 @@ DEPENDENCIES
omniauth_openid_connect (~> 0.6.1)
ox (~> 2.14)
parslet
pg (~> 1.4)
pg (~> 1.5)
pghero
pkg-config (~> 1.5)
posix-spawn
@ -849,7 +856,7 @@ DEPENDENCIES
public_suffix (~> 5.0)
puma (~> 6.2)
pundit (~> 2.3)
rack (~> 2.2.6)
rack (~> 2.2.7)
rack-attack (~> 6.6)
rack-cors (~> 2.0)
rack-test (~> 2.1)
@ -871,7 +878,7 @@ DEPENDENCIES
rubocop-performance
rubocop-rails
rubocop-rspec
ruby-progressbar (~> 1.11)
ruby-progressbar (~> 1.13)
sanitize (~> 6.0)
scenic (~> 1.7)
sidekiq (~> 6.5)

@ -8,7 +8,7 @@ class AboutController < ApplicationController
before_action :set_instance_presenter
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
private

@ -7,8 +7,9 @@ class AccountsController < ApplicationController
include AccountControllerConcern
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 :set_cache_headers
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
skip_before_action :require_functional!, unless: :whitelist_mode?
@ -16,7 +17,7 @@ class AccountsController < ApplicationController
def show
respond_to do |format|
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
end

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,11 +1,12 @@
# frozen_string_literal: true
class Api::V1::CustomEmojisController < Api::BaseController
vary_by '', unless: :disallow_unauthenticated_api_access?
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
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) }
end
end

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

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

@ -6,8 +6,15 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
before_action :require_enabled_api!
before_action :set_domain_blocks
vary_by '', if: -> { Setting.show_domain_blocks == 'all' }
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?))
end

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

@ -3,11 +3,18 @@
class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api!
skip_before_action :set_cache_headers
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
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) }
end

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

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

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

@ -1,11 +1,18 @@
# frozen_string_literal: true
class Api::V1::InstancesController < Api::BaseController
skip_before_action :set_cache_headers
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
expires_in 3.minutes, public: true
cache_even_if_authenticated!
render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance'
end
end

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

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

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

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

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

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

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

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

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

@ -1,12 +1,15 @@
# frozen_string_literal: true
class Api::V1::Trends::StatusesController < Api::BaseController
vary_by 'Authorization, Accept-Language'
before_action :require_user!, only: [:index], if: :require_auth?
before_action :set_statuses
after_action :insert_pagination_headers
def index
cache_if_unauthenticated!
render json: @statuses, each_serializer: REST::StatusSerializer
end

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

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

@ -21,6 +21,8 @@ class ApplicationController < ActionController::Base
helper_method :omniauth_only?
helper_method :sso_account_settings
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 Mastodon::NotPermittedError, with: :forbidden
@ -37,9 +39,11 @@ class ApplicationController < ActionController::Base
service_unavailable
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 :set_cache_control_defaults
skip_before_action :verify_authenticity_token, only: :raise_not_found
def raise_not_found
@ -56,14 +60,25 @@ class ApplicationController < ActionController::Base
!authorized_fetch_mode?
end
def store_current_location
store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym)
def store_referrer
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
def require_functional!
redirect_to edit_user_registration_path unless current_user.functional?
end
def skip_csrf_meta_tags?
false
end
def after_sign_out_path_for(_resource_or_scope)
if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true'
'/auth/auth/openid_connect/logout'
@ -127,7 +142,7 @@ class ApplicationController < ActionController::Base
end
def sso_account_settings
ENV.fetch('SSO_ACCOUNT_SETTINGS')
ENV.fetch('SSO_ACCOUNT_SETTINGS', nil)
end
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?
end
def body_class_string
@body_classes || ''
end
def respond_with_error(code)
respond_to do |format|
format.any do
@ -151,4 +170,8 @@ class ApplicationController < ActionController::Base
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
end
end
def set_cache_control_defaults
response.cache_control.replace(private: true, no_store: true)
end
end

@ -157,6 +157,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def set_cache_headers
response.headers['Cache-Control'] = 'private, no-store'
response.cache_control.replace(private: true, no_store: true)
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
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)
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(':')
expires_in = options.delete(:expires_in) || 3.minutes
@ -176,10 +198,6 @@ module CacheConcern
end
end
def set_cache_headers
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
end
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)

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

@ -1,18 +1,8 @@
# frozen_string_literal: true
class CustomCssController < 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
class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
def show
expires_in 3.minutes, public: true
request.session_options[:skip] = true
render content_type: 'text/css'
end
end

@ -10,6 +10,7 @@ class Disputes::BaseController < ApplicationController
before_action :set_body_classes
before_action :authenticate_user!
before_action :set_pack
before_action :set_cache_headers
private
@ -20,4 +21,8 @@ class Disputes::BaseController < ApplicationController
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.cache_control.replace(private: true, no_store: true)
end
end

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

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

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

@ -5,8 +5,9 @@ class FollowerAccountsController < ApplicationController
include SignatureVerification
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 :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :whitelist_mode?
@ -14,7 +15,7 @@ class FollowerAccountsController < ApplicationController
def index
respond_to do |format|
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
format.json do

@ -5,8 +5,9 @@ class FollowingAccountsController < ApplicationController
include SignatureVerification
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 :set_cache_headers
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, unless: :whitelist_mode?
@ -14,7 +15,7 @@ class FollowingAccountsController < ApplicationController
def index
respond_to do |format|
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
format.json do

@ -6,7 +6,7 @@ class HomeController < ApplicationController
before_action :set_instance_presenter
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
private

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

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

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

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

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

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

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

@ -8,7 +8,7 @@ class PrivacyController < ApplicationController
before_action :set_instance_presenter
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
private

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

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

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

@ -6,11 +6,12 @@ class StatusesController < ApplicationController
include Authorization
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 :set_status
before_action :set_instance_presenter
before_action :redirect_to_original, only: :show
before_action :set_cache_headers
before_action :set_body_classes, only: :embed
after_action :set_link_headers
@ -29,7 +30,7 @@ class StatusesController < ApplicationController
end
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
end
end

@ -7,6 +7,8 @@ class TagsController < ApplicationController
PAGE_SIZE = 20
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 :authenticate_user!, if: :whitelist_mode?
before_action :set_local
@ -19,7 +21,7 @@ class TagsController < ApplicationController
def show
respond_to do |format|
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
format.rss do

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

@ -1,10 +1,12 @@
# frozen_string_literal: true
module WellKnown
class NodeInfoController < ActionController::Base
class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController
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
expires_in 3.days, public: true

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

@ -155,20 +155,8 @@ module ApplicationHelper
tag(:meta, content: content, property: property)
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
output = (@body_classes || '').split
output = body_class_string.split
output << "flavour-#{current_flavour.parameterize}"
output << "skin-#{current_skin.parameterize}"
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
end
def description_for_sign_up
prefix = 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
def description_for_sign_up(invite = nil)
safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ')
end
private
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

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

@ -201,7 +201,6 @@ module LanguagesHelper
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
szl: ['Silesian', 'ślůnsko godka'].freeze,
tai: ['Tai', 'ภาษาไท or ภาษาไต'].freeze,
tok: ['Toki Pona', 'toki pona'].freeze,
zba: ['Balaibalan', 'باليبلن'].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
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?
params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION
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?
ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif
end

@ -32,7 +32,7 @@ class EditedTimestamp extends React.PureComponent {
renderHeader = items => {
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 }} />
);
};

@ -41,7 +41,7 @@ class SilentErrorBoundary extends React.Component {
export const accountsCountRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
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={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,

@ -1,10 +1,15 @@
import React from 'react';
import logo from 'mastodon/../images/logo.svg';
const Logo = () => (
<svg viewBox='0 0 261 66' className='logo' role='img'>
export const WordmarkLogo = () => (
<svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
<title>Mastodon</title>
<use xlinkHref='#logo-symbol-wordmark' />
</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 [, division] = shortNumber;
// eslint-disable-next-line eqeqeq
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.');
}
// eslint-disable-next-line eqeqeq
const customRenderer = children != null ? children : renderer;
const displayNumber = <ShortNumberCounter value={shortNumber} />;
// eslint-disable-next-line eqeqeq
return customRenderer != null
? customRenderer(displayNumber, pluralReady(value, division))
: displayNumber;

@ -69,6 +69,9 @@ class Status extends ImmutablePureComponent {
id: PropTypes.string,
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
previousId: PropTypes.string,
nextInReplyToId: PropTypes.string,
rootId: PropTypes.string,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
@ -522,6 +525,9 @@ class Status extends ImmutablePureComponent {
unread,
featured,
pictureInPicture,
previousId,
nextInReplyToId,
rootId,
...other
} = this.props;
const { isCollapsed, forceFilter } = this.state;
@ -565,6 +571,8 @@ class Status extends ImmutablePureComponent {
openMedia: this.handleHotkeyOpenMedia,
};
let prepend, rebloggedByText;
if (hidden) {
return (
<HotKeys handlers={handlers}>
@ -576,7 +584,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');
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
@ -667,7 +679,7 @@ class Status extends ImmutablePureComponent {
inline
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
preventPlayback={isCollapsed || !isExpanded}
onOpenVideo={this.handleOpenVideo}
width={this.props.cachedMediaWidth}
@ -688,7 +700,7 @@ class Status extends ImmutablePureComponent {
lang={status.get('language')}
sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
hidden={isCollapsed || !isExpanded}
onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
@ -730,8 +742,6 @@ class Status extends ImmutablePureComponent {
'data-status-by': `@${status.getIn(['account', 'acct'])}`,
};
let prepend;
if (this.props.prepend && account) {
const notifKind = {
favourite: 'favourited',
@ -753,8 +763,6 @@ class Status extends ImmutablePureComponent {
);
}
let rebloggedByText;
if (this.props.prepend === 'reblog') {
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') });
}
@ -763,6 +771,8 @@ class Status extends ImmutablePureComponent {
collapsed: isCollapsed,
'has-background': isCollapsed && background,
'status__wrapper-reply': !!status.get('in_reply_to_id'),
'status--in-thread': !!rootId,
'status--first-in-thread': previousId && (!connectUp || connectToRoot),
unread,
muted,
}, 'focusable');
@ -779,6 +789,9 @@ class Status extends ImmutablePureComponent {
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
>
{!muted && prepend}
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
<header className='status__info'>
<span>
{muted && prepend}

@ -85,6 +85,7 @@ const makeMapStateToProps = () => {
return {
containerId: props.containerId || props.id, // Should match reblogStatus's id for reblogs
status: status,
nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null,
account: account || props.account,
settings: state.get('local_settings'),
prepend: prepend || props.prepend,

@ -127,7 +127,7 @@ class SearchResults extends ImmutablePureComponent {
<div className='drawer--results'>
<header className='search-results__header'>
<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 }} />
</header>
{accounts}

@ -20,68 +20,88 @@ const emojiFilename = (filename) => {
};
const emojifyTextNode = (node, customEmojis) => {
const VS15 = 0xFE0E;
const VS16 = 0xFE0F;
let str = node.textContent;
const fragment = new DocumentFragment();
let i = 0;
for (;;) {
let match, i = 0;
let unicode_emoji;
// Skip to the next potential emoji to replace (either custom emoji or custom emoji :shortcode:)
if (customEmojis === null) {
while (i < str.length && (useSystemEmojiFont || !(match = trie.search(str.slice(i))))) {
while (i < str.length && (useSystemEmojiFont || !(unicode_emoji = trie.search(str.slice(i))))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
} else {
while (i < str.length && str[i] !== ':' && (useSystemEmojiFont || !(match = trie.search(str.slice(i))))) {
while (i < str.length && str[i] !== ':' && (useSystemEmojiFont || !(unicode_emoji = trie.search(str.slice(i))))) {
i += str.codePointAt(i) < 65536 ? 1 : 2;
}
}
let rend, replacement = null;
// We reached the end of the string, nothing to replace
if (i === str.length) {
break;
} else if (str[i] === ':') {
if (!(() => {
rend = str.indexOf(':', i + 1) + 1;
if (!rend) return false; // no pair of ':'
const shortname = str.slice(i, rend);
// now got a replacee as ':shortname:'
// if you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) {
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = document.createElement('img');
replacement.setAttribute('draggable', 'false');
replacement.setAttribute('class', 'emojione custom-emoji');
replacement.setAttribute('alt', shortname);
replacement.setAttribute('title', shortname);
replacement.setAttribute('src', filename);
replacement.setAttribute('data-original', customEmojis[shortname].url);
replacement.setAttribute('data-static', customEmojis[shortname].static_url);
return true;
}
return false;
})()) rend = ++i;
} else if (!useSystemEmojiFont) { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match];
}
let rend, replacement = null;
if (str[i] === ':') { // Potentially the start of a custom emoji :shortcode:
rend = str.indexOf(':', i + 1) + 1;
// no matching ending ':', skip
if (!rend) {
i++;
continue;
}
const shortcode = str.slice(i, rend);
const custom_emoji = customEmojis[shortcode];
// not a recognized shortcode, skip
if (!custom_emoji) {
i++;
continue;
}
// now got a replacee as ':shortcode:'
// if you want additional emoji handler, add statements below which set replacement and return true.
const filename = autoPlayGif ? custom_emoji.url : custom_emoji.static_url;
replacement = document.createElement('img');
replacement.setAttribute('draggable', 'false');
replacement.setAttribute('class', 'emojione custom-emoji');
replacement.setAttribute('alt', shortcode);
replacement.setAttribute('title', shortcode);
replacement.setAttribute('src', filename);
replacement.setAttribute('data-original', custom_emoji.url);
replacement.setAttribute('data-static', custom_emoji.static_url);
} else { // start of an unicode emoji
rend = i + unicode_emoji.length;
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
if (str.codePointAt(rend - 1) !== VS16 && str.codePointAt(rend) === VS15) {
i = rend + 1;
continue;
}
const { filename, shortCode } = unicodeMapping[unicode_emoji];
const title = shortCode ? `:${shortCode}:` : '';
replacement = document.createElement('img');
replacement.setAttribute('draggable', 'false');
replacement.setAttribute('class', 'emojione');
replacement.setAttribute('alt', match);
replacement.setAttribute('alt', unicode_emoji);
replacement.setAttribute('title', title);
replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`);
rend = i + match.length;
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
if (str.codePointAt(rend) === 65038) {
rend += 1;
}
}
// Add the processed-up-to-now string and the emoji replacement
fragment.append(document.createTextNode(str.slice(0, i)));
if (replacement) {
fragment.append(replacement);
}
fragment.append(replacement);
str = str.slice(rend);
i = 0;
}
fragment.append(document.createTextNode(str));

@ -71,17 +71,20 @@ class Explore extends React.PureComponent {
<NavLink exact to='/explore'>
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' />
</NavLink>
<NavLink exact to='/explore/tags'>
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' />
</NavLink>
<NavLink exact to='/explore/links'>
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
</NavLink>
{signedIn && (
<NavLink exact to='/explore/suggestions'>
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='For you' />
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' />
</NavLink>
)}
<NavLink exact to='/explore/links'>
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' />
</NavLink>
</div>
<Switch>

@ -45,7 +45,7 @@ class Report extends ImmutablePureComponent {
<div className='notification__report__details'>
<div>
<RelativeTimestamp timestamp={report.get('created_at')} short={false} /> · <FormattedMessage id='report_notification.attached_statuses' defaultMessage='{count, plural, one {{count} post} other {{count} posts}} attached' values={{ count: report.get('status_ids').size }} />
<RelativeTimestamp timestamp={report.get('created_at')} short={false} /> · <FormattedMessage id='report_notification.attached_statuses' defaultMessage='{count, plural, one {# post} other {# posts}} attached' values={{ count: report.get('status_ids').size }} />
<br />
<strong>{intl.formatMessage(messages[report.get('category')])}</strong>
</div>

@ -17,16 +17,6 @@ const getHostname = url => {
return parser.hostname;
};
const trim = (text, len) => {
const cut = text.indexOf(' ', len);
if (cut === -1) {
return text;
}
return text.slice(0, cut) + (text.length > len ? '…' : '');
};
const domParser = new DOMParser();
const addAutoPlay = html => {
@ -54,7 +44,6 @@ export default class Card extends React.PureComponent {
static propTypes = {
card: ImmutablePropTypes.map,
maxDescription: PropTypes.number,
onOpenMedia: PropTypes.func.isRequired,
compact: PropTypes.bool,
defaultWidth: PropTypes.number,
@ -63,7 +52,6 @@ export default class Card extends React.PureComponent {
};
static defaultProps = {
maxDescription: 50,
compact: false,
};
@ -176,7 +164,7 @@ export default class Card extends React.PureComponent {
}
render () {
const { card, maxDescription, compact, defaultWidth } = this.props;
const { card, compact, defaultWidth } = this.props;
const { width, embedded, revealed } = this.state;
if (card === null) {
@ -195,7 +183,7 @@ export default class Card extends React.PureComponent {
const description = (
<div className='status-card__content' lang={language}>
{title}
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
{!(horizontal || compact) && <p className='status-card__description' title={card.get('description')}>{card.get('description')}</p>}
<span className='status-card__host'>{provider}</span>
</div>
);

@ -65,7 +65,7 @@ const messages = defineMessages({
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
@ -574,8 +574,10 @@ class Status extends ImmutablePureComponent {
this.column.scrollTop();
};
renderChildren (list) {
return list.map(id => (
renderChildren (list, ancestors) {
const { params: { statusId } } = this.props;
return list.map((id, i) => (
<StatusContainer
key={id}
id={id}
@ -583,6 +585,9 @@ class Status extends ImmutablePureComponent {
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
previousId={i > 0 && list.get(i - 1)}
nextId={list.get(i + 1) || (ancestors && statusId)}
rootId={statusId}
/>
));
}
@ -643,7 +648,7 @@ class Status extends ImmutablePureComponent {
const isExpanded = settings.getIn(['content_warnings', 'shared_state']) ? !status.get('hidden') : this.state.isExpanded;
if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <>{this.renderChildren(ancestorsIds)}</>;
ancestors = <>{this.renderChildren(ancestorsIds, true)}</>;
}
if (descendantsIds && descendantsIds.size > 0) {

@ -1,5 +1,5 @@
import React from 'react';
import Logo from 'flavours/glitch/components/logo';
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
import { Link, withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';
import { registrationsOpen, me } from 'flavours/glitch/initial_state';
@ -74,7 +74,10 @@ class Header extends React.PureComponent {
return (
<div className='ui__header'>
<Link to='/' className='ui__header__logo'><Logo /></Link>
<Link to='/' className='ui__header__logo'>
<WordmarkLogo />
<SymbolLogo />
</Link>
<div className='ui__header__links'>
{content}

@ -1,4 +1,8 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "Done",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Plasings van mense wat jy volg, kom chronologies in jou tuisvoer verby. Moenie huiwer nie. Volg na hartelus. As daar mense is wie se plasings jy nie meer wil sien nie, ontvolg hulle net!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

@ -1,4 +1,8 @@
{
"empty_column.follow_recommendations": "Pareixe que no s'ha puesto chenerar garra sucherencia pa tu. Puetz prebar a buscar a chent que talment conoixcas u explorar los hashtags que son en tendencia.",
"follow_recommendations.done": "Feito",
"follow_recommendations.heading": "Sigue a chent que publique cosetas que te faigan goyo! Aquí tiens qualques sucherencias.",
"follow_recommendations.lead": "Las publicacions d'a chent a la quala sigas amaneixerán ordenadas cronolochicament en Inicio. No tiengas miedo de cometer errors, puetz deixar-les de seguir en qualsequier momento con a mesma facilidat!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

@ -1,4 +1,8 @@
{
"empty_column.follow_recommendations": "يبدو أنه لا يمكن إنشاء أي اقتراحات لك. يمكنك البحث عن أشخاص قد تعرفهم أو استكشاف الوسوم الرائجة.",
"follow_recommendations.done": "تم",
"follow_recommendations.heading": "تابع الأشخاص الذين ترغب في رؤية منشوراتهم! إليك بعض الاقتراحات.",
"follow_recommendations.lead": "ستظهر منشورات الأشخاص الذين تُتابعتهم بترتيب تسلسلي زمني على صفحتك الرئيسية. لا تخف إذا ارتكبت أي أخطاء، تستطيع إلغاء متابعة أي شخص في أي وقت تريد!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

@ -1,4 +1,8 @@
{
"empty_column.follow_recommendations": "Paez que nun se puen xenerar suxerencies pa ti. Pues tentar d'usar la busca p'atopar perfiles que pues conocer o esplorar les etiquetes en tendencia.",
"follow_recommendations.done": "Fecho",
"follow_recommendations.heading": "¡Sigui a perfiles que te prestaría ver nel feed personal! Equí tienes dalgunes suxerencies.",
"follow_recommendations.lead": "Los artículos de los perfiles que sigas van apaecer n'orde cronolóxicu nel to feed d'aniciu. ¡Nun tengas mieu d'enquivocate, pues dexar de siguilos con facilidá en cualesquier momentu!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

@ -1,4 +1,8 @@
{
"empty_column.follow_recommendations": "Здаецца, прапаноў для вас няма. Вы можаце паспрабаваць выкарыстаць пошук, каб знайсці людзей, якіх вы можаце ведаць, ці даследаваць папулярныя хэштэгі.",
"follow_recommendations.done": "Гатова",
"follow_recommendations.heading": "Падпісвайцеся на людзей, допісы якіх вам будуць цікавы! Вось некаторыя рэкамендацыі.",
"follow_recommendations.lead": "Допісы людзей, на якіх вы падпісаны, будуць паказаны ў храналагічным парадку на вашай хатняй старонцы. Не бойцеся памыляцца, вы лёгка зможаце адпісацца ў любы момант!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

@ -1,4 +1,8 @@
{
"empty_column.follow_recommendations": "Изглежда, че няма предложения, които може да се породят за вас. Може да опитате да потърсите хора, които познавате или да разгледате налагащи се хаштагове.",
"follow_recommendations.done": "Готово",
"follow_recommendations.heading": "Следвайте хора, от които харесвате да виждате публикации! Ето някои предложения.",
"follow_recommendations.lead": "Публикациите от последваните, ще се показват в хронологичен ред в началния ви инфоканал. Не се страхувайте, че ще сгрешите, по всяко време много лесно може да спрете да ги следвате!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

@ -1,4 +1,8 @@
{
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"follow_recommendations.done": "সম্পন্ন",
"follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.",
"follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

@ -1,4 +1,8 @@
{
"empty_column.follow_recommendations": "War a seblant ne c'hall ket bezañ savet erbedadenn ebet evidoc'h. Gallout a rit implijout un enklask evit kavout tud a anavezfec'h pe furchal ar gerioù-klik diouzh ar c'hiz.",
"follow_recommendations.done": "Graet",
"follow_recommendations.heading": "Heuilhit tud a blijfe deoc'h lenn o zoudoù ! Setu un nebeud erbedadennoù.",
"follow_recommendations.lead": "Toudoù gant tud a vez heuliet ganeoc'h a zeuio war wel en urzh kronologel war ho red degemer. Arabat kaout aon ober fazioù, diheuliañ tud a c'hellit ober aes ha forzh pegoulz !",
"onboarding.page_one.federation": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.",
"onboarding.page_six.github": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.",
"settings.content_warnings": "Content warnings",

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

Loading…
Cancel
Save