Merge pull request #2215 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						f08f6d20e6
					
				| 
						 | 
					@ -43,9 +43,16 @@ jobs:
 | 
				
			||||||
            type=edge,branch=main
 | 
					            type=edge,branch=main
 | 
				
			||||||
            type=sha,prefix=,format=long
 | 
					            type=sha,prefix=,format=long
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Generate version suffix
 | 
				
			||||||
 | 
					        id: version_vars
 | 
				
			||||||
 | 
					        if: github.repository == 'mastodon/mastodon' && github.event_name == 'push' && github.ref_name == 'main'
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          echo mastodon_version_suffix=+edge-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - uses: docker/build-push-action@v4
 | 
					      - uses: docker/build-push-action@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          context: .
 | 
					          context: .
 | 
				
			||||||
 | 
					          build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }}
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
          provenance: false
 | 
					          provenance: false
 | 
				
			||||||
          builder: ${{ steps.buildx.outputs.name }}
 | 
					          builder: ${{ steps.buildx.outputs.name }}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,9 +41,15 @@ jobs:
 | 
				
			||||||
          labels: |
 | 
					          labels: |
 | 
				
			||||||
            org.opencontainers.image.description=Nightly build image used for testing purposes
 | 
					            org.opencontainers.image.description=Nightly build image used for testing purposes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Generate version suffix
 | 
				
			||||||
 | 
					        id: version_vars
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          echo mastodon_version_suffix=+nightly-$(date +'%Y%m%d') >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - uses: docker/build-push-action@v4
 | 
					      - uses: docker/build-push-action@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          context: .
 | 
					          context: .
 | 
				
			||||||
 | 
					          build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }}
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
          provenance: false
 | 
					          provenance: false
 | 
				
			||||||
          builder: ${{ steps.buildx.outputs.name }}
 | 
					          builder: ${{ steps.buildx.outputs.name }}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,7 +48,7 @@ jobs:
 | 
				
			||||||
        run: yarn --frozen-lockfile
 | 
					        run: yarn --frozen-lockfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: ESLint
 | 
					      - name: ESLint
 | 
				
			||||||
        run: yarn test:lint:js
 | 
					        run: yarn test:lint:js --max-warnings 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Typecheck
 | 
					      - name: Typecheck
 | 
				
			||||||
        run: yarn test:typecheck
 | 
					        run: yarn test:typecheck
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,6 @@ on:
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
  BUNDLE_CLEAN: true
 | 
					  BUNDLE_CLEAN: true
 | 
				
			||||||
  BUNDLE_FROZEN: true
 | 
					  BUNDLE_FROZEN: true
 | 
				
			||||||
  BUNDLE_WITHOUT: 'development production'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
concurrency:
 | 
					concurrency:
 | 
				
			||||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
					  group: ${{ github.workflow }}-${{ github.ref }}
 | 
				
			||||||
| 
						 | 
					@ -19,8 +18,17 @@ jobs:
 | 
				
			||||||
  build:
 | 
					  build:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: true
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        mode:
 | 
				
			||||||
 | 
					          - production
 | 
				
			||||||
 | 
					          - test
 | 
				
			||||||
    env:
 | 
					    env:
 | 
				
			||||||
      RAILS_ENV: test
 | 
					      RAILS_ENV: ${{ matrix.mode }}
 | 
				
			||||||
 | 
					      BUNDLE_WITH: ${{ matrix.mode }}
 | 
				
			||||||
 | 
					      OTP_SECRET: precompile_placeholder
 | 
				
			||||||
 | 
					      SECRET_KEY_BASE: precompile_placeholder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v3
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
| 
						 | 
					@ -50,6 +58,7 @@ jobs:
 | 
				
			||||||
          ./bin/rails assets:precompile
 | 
					          ./bin/rails assets:precompile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - uses: actions/upload-artifact@v3
 | 
					      - uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        if: matrix.mode == 'test'
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          path: |-
 | 
					          path: |-
 | 
				
			||||||
            ./public/assets
 | 
					            ./public/assets
 | 
				
			||||||
| 
						 | 
					@ -97,7 +106,7 @@ jobs:
 | 
				
			||||||
      PAM_ENABLED: true
 | 
					      PAM_ENABLED: true
 | 
				
			||||||
      PAM_DEFAULT_SERVICE: pam_test
 | 
					      PAM_DEFAULT_SERVICE: pam_test
 | 
				
			||||||
      PAM_CONTROLLED_SERVICE: pam_test_controlled
 | 
					      PAM_CONTROLLED_SERVICE: pam_test_controlled
 | 
				
			||||||
      BUNDLE_WITH: 'pam_authentication'
 | 
					      BUNDLE_WITH: 'pam_authentication test'
 | 
				
			||||||
      CI_JOBS: ${{ matrix.ci_job }}/4
 | 
					      CI_JOBS: ${{ matrix.ci_job }}/4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    strategy:
 | 
					    strategy:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								.profile
								
								
								
								
							
							
						
						
									
										2
									
								
								.profile
								
								
								
								
							| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio
 | 
					LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,6 +65,7 @@ Metrics/AbcSize:
 | 
				
			||||||
Metrics/BlockLength:
 | 
					Metrics/BlockLength:
 | 
				
			||||||
  CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
 | 
					  CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
 | 
					    - 'config/routes.rb'
 | 
				
			||||||
    - 'lib/mastodon/*_cli.rb'
 | 
					    - 'lib/mastodon/*_cli.rb'
 | 
				
			||||||
    - 'lib/tasks/*.rake'
 | 
					    - 'lib/tasks/*.rake'
 | 
				
			||||||
    - 'app/models/concerns/account_associations.rb'
 | 
					    - 'app/models/concerns/account_associations.rb'
 | 
				
			||||||
| 
						 | 
					@ -85,6 +86,7 @@ Metrics/BlockLength:
 | 
				
			||||||
    - 'config/initializers/simple_form.rb'
 | 
					    - 'config/initializers/simple_form.rb'
 | 
				
			||||||
    - 'config/navigation.rb'
 | 
					    - 'config/navigation.rb'
 | 
				
			||||||
    - 'config/routes.rb'
 | 
					    - 'config/routes.rb'
 | 
				
			||||||
 | 
					    - 'config/routes/*.rb'
 | 
				
			||||||
    - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb'
 | 
					    - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb'
 | 
				
			||||||
    - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb'
 | 
					    - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb'
 | 
				
			||||||
    - 'lib/paperclip/gif_transcoder.rb'
 | 
					    - 'lib/paperclip/gif_transcoder.rb'
 | 
				
			||||||
| 
						 | 
					@ -130,6 +132,7 @@ Metrics/ClassLength:
 | 
				
			||||||
    - 'app/services/activitypub/process_account_service.rb'
 | 
					    - 'app/services/activitypub/process_account_service.rb'
 | 
				
			||||||
    - 'app/services/activitypub/process_status_update_service.rb'
 | 
					    - 'app/services/activitypub/process_status_update_service.rb'
 | 
				
			||||||
    - 'app/services/backup_service.rb'
 | 
					    - 'app/services/backup_service.rb'
 | 
				
			||||||
 | 
					    - 'app/services/bulk_import_service.rb'
 | 
				
			||||||
    - 'app/services/delete_account_service.rb'
 | 
					    - 'app/services/delete_account_service.rb'
 | 
				
			||||||
    - 'app/services/fan_out_on_write_service.rb'
 | 
					    - 'app/services/fan_out_on_write_service.rb'
 | 
				
			||||||
    - 'app/services/fetch_link_card_service.rb'
 | 
					    - 'app/services/fetch_link_card_service.rb'
 | 
				
			||||||
| 
						 | 
					@ -158,6 +161,11 @@ Metrics/MethodLength:
 | 
				
			||||||
Metrics/ModuleLength:
 | 
					Metrics/ModuleLength:
 | 
				
			||||||
  CountAsOne: [array, heredoc]
 | 
					  CountAsOne: [array, heredoc]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Reason: Prevailing style is argument file paths
 | 
				
			||||||
 | 
					# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath
 | 
				
			||||||
 | 
					Rails/FilePath:
 | 
				
			||||||
 | 
					  EnforcedStyle: arguments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus
 | 
					# Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus
 | 
				
			||||||
# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus
 | 
					# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus
 | 
				
			||||||
Rails/HttpStatus:
 | 
					Rails/HttpStatus:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,13 +21,6 @@ Layout/ArgumentAlignment:
 | 
				
			||||||
    - 'config/initializers/cors.rb'
 | 
					    - 'config/initializers/cors.rb'
 | 
				
			||||||
    - 'config/initializers/session_store.rb'
 | 
					    - 'config/initializers/session_store.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					 | 
				
			||||||
# Configuration parameters: EnforcedStyle.
 | 
					 | 
				
			||||||
# SupportedStyles: empty_lines, no_empty_lines
 | 
					 | 
				
			||||||
Layout/EmptyLinesAroundBlockBody:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'config/routes.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
 | 
					# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
 | 
				
			||||||
Layout/ExtraSpacing:
 | 
					Layout/ExtraSpacing:
 | 
				
			||||||
| 
						 | 
					@ -106,28 +99,6 @@ Lint/AmbiguousOperatorPrecedence:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'config/initializers/rack_attack.rb'
 | 
					    - 'config/initializers/rack_attack.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: AllowedMethods.
 | 
					 | 
				
			||||||
# AllowedMethods: enums
 | 
					 | 
				
			||||||
Lint/ConstantDefinitionInBlock:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/base_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/application_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/accountable_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/signature_verification_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/activitypub/adapter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/connection_pool/shared_connection_pool_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/connection_pool/shared_timed_stack_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/concerns/remotable_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches.
 | 
					 | 
				
			||||||
Lint/DuplicateBranch:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/lib/permalink_redirector.rb'
 | 
					 | 
				
			||||||
    - 'app/models/account_statuses_filter.rb'
 | 
					 | 
				
			||||||
    - 'app/validators/email_mx_validator.rb'
 | 
					 | 
				
			||||||
    - 'app/validators/vote_validator.rb'
 | 
					 | 
				
			||||||
    - 'lib/mastodon/maintenance_cli.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration parameters: AllowComments, AllowEmptyLambdas.
 | 
					# Configuration parameters: AllowComments, AllowEmptyLambdas.
 | 
				
			||||||
Lint/EmptyBlock:
 | 
					Lint/EmptyBlock:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
| 
						 | 
					@ -168,11 +139,6 @@ Lint/EmptyBlock:
 | 
				
			||||||
    - 'spec/models/user_role_spec.rb'
 | 
					    - 'spec/models/user_role_spec.rb'
 | 
				
			||||||
    - 'spec/models/web/setting_spec.rb'
 | 
					    - 'spec/models/web/setting_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: AllowComments.
 | 
					 | 
				
			||||||
Lint/EmptyClass:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/base_controller_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Lint/NonLocalExitFromIterator:
 | 
					Lint/NonLocalExitFromIterator:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/helpers/jsonld_helper.rb'
 | 
					    - 'app/helpers/jsonld_helper.rb'
 | 
				
			||||||
| 
						 | 
					@ -228,6 +194,12 @@ Metrics/AbcSize:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/serializers/initial_state_serializer.rb'
 | 
					    - 'app/serializers/initial_state_serializer.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
 | 
				
			||||||
 | 
					# AllowedMethods: refine
 | 
				
			||||||
 | 
					Metrics/BlockLength:
 | 
				
			||||||
 | 
					  Exclude:
 | 
				
			||||||
 | 
					    - 'app/models/concerns/status_safe_reblog_insert.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: CountBlocks, Max.
 | 
					# Configuration parameters: CountBlocks, Max.
 | 
				
			||||||
Metrics/BlockNesting:
 | 
					Metrics/BlockNesting:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
| 
						 | 
					@ -305,42 +277,6 @@ Naming/VariableNumber:
 | 
				
			||||||
    - 'spec/models/user_spec.rb'
 | 
					    - 'spec/models/user_spec.rb'
 | 
				
			||||||
    - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb'
 | 
					    - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: MinSize.
 | 
					 | 
				
			||||||
Performance/CollectionLiteralInLoop:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/models/admin/appeal_filter.rb'
 | 
					 | 
				
			||||||
    - 'app/models/admin/status_filter.rb'
 | 
					 | 
				
			||||||
    - 'app/models/relationship_filter.rb'
 | 
					 | 
				
			||||||
    - 'app/models/trends/preview_card_filter.rb'
 | 
					 | 
				
			||||||
    - 'app/models/trends/status_filter.rb'
 | 
					 | 
				
			||||||
    - 'app/presenters/status_relationships_presenter.rb'
 | 
					 | 
				
			||||||
    - 'app/services/fetch_resource_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/suspend_account_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/unsuspend_account_service.rb'
 | 
					 | 
				
			||||||
    - 'config/deploy.rb'
 | 
					 | 
				
			||||||
    - 'lib/mastodon/media_cli.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
Performance/Count:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/lib/importer/accounts_index_importer.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/importer/tags_index_importer.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
# Configuration parameters: SafeMultiline.
 | 
					 | 
				
			||||||
Performance/DeletePrefix:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/controllers/authorize_interactions_controller.rb'
 | 
					 | 
				
			||||||
    - 'app/controllers/concerns/signature_verification.rb'
 | 
					 | 
				
			||||||
    - 'app/controllers/intents_controller.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/activitypub/case_transform.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/permalink_redirector.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/webfinger_resource.rb'
 | 
					 | 
				
			||||||
    - 'app/services/activitypub/fetch_remote_actor_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/backup_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/resolve_account_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/tag_search_service.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
Performance/MapCompact:
 | 
					Performance/MapCompact:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
| 
						 | 
					@ -360,46 +296,12 @@ Performance/MapCompact:
 | 
				
			||||||
    - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
 | 
					    - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
 | 
				
			||||||
    - 'spec/presenters/status_relationships_presenter_spec.rb'
 | 
					    - 'spec/presenters/status_relationships_presenter_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Performance/MethodObjectAsBlock:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/models/account_suggestions/source.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/export_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
# Configuration parameters: AllowRegexpMatch.
 | 
					 | 
				
			||||||
Performance/RedundantEqualityComparisonBlock:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/requests/link_headers_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
# Configuration parameters: MaxKeyValuePairs.
 | 
					 | 
				
			||||||
Performance/RedundantMerge:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'config/initializers/paperclip.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
# Configuration parameters: SafeMultiline.
 | 
					# Configuration parameters: SafeMultiline.
 | 
				
			||||||
Performance/StartWith:
 | 
					Performance/StartWith:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/lib/extractor.rb'
 | 
					    - 'app/lib/extractor.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
# Configuration parameters: OnlySumOrWithInitialValue.
 | 
					 | 
				
			||||||
Performance/Sum:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/lib/activity_tracker.rb'
 | 
					 | 
				
			||||||
    - 'app/models/trends/history.rb'
 | 
					 | 
				
			||||||
    - 'lib/paperclip/color_extractor.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
Performance/TimesMap:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/mutes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/feed_manager_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/request_pool_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
Performance/UnfreezeString:
 | 
					Performance/UnfreezeString:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
| 
						 | 
					@ -428,116 +330,6 @@ RSpec/AnyInstance:
 | 
				
			||||||
    - 'spec/workers/activitypub/delivery_worker_spec.rb'
 | 
					    - 'spec/workers/activitypub/delivery_worker_spec.rb'
 | 
				
			||||||
    - 'spec/workers/web/push_notification_worker_spec.rb'
 | 
					    - 'spec/workers/web/push_notification_worker_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: Prefixes, AllowedPatterns.
 | 
					 | 
				
			||||||
# Prefixes: when, with, without
 | 
					 | 
				
			||||||
RSpec/ContextWording:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/config/initializers/rack_attack_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/collections_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/inboxes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/domain_blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/reports/actions_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/statuses_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/accounts/relationships_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/emails/confirmations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/instances/activity_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/instances/peers_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/media_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v2/filters_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/application_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/auth/registrations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/auth/sessions_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/cache_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/challengable_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/localized_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/rate_limit_headers_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/instance_actors_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/settings/applications_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/statuses_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/jsonld_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/routing_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/activitypub/activity/accept_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/activitypub/activity/announce_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/activitypub/activity/create_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/activitypub/activity/follow_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/activitypub/activity/reject_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/advanced_text_formatter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/emoji_formatter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/entity_cache_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/feed_manager_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/html_aware_formatter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/link_details_extractor_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/ostatus/tag_manager_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/scope_transformer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/status_cache_hydrator_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/status_reach_finder_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/text_formatter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account/field_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/admin/account_action_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/concerns/account_interactions_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/concerns/remotable_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/custom_emoji_filter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/custom_emoji_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/email_domain_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/media_attachment_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/notification_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/remote_follow_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/report_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/session_activation_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/setting_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/status_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/web/push_subscription_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/account_moderation_note_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/account_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/backup_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/custom_emoji_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/domain_block_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/email_domain_block_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/instance_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/invite_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/relay_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/report_note_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/report_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/settings_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/tag_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/policies/user_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/presenters/account_relationships_presenter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/presenters/status_relationships_presenter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/account_search_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/account_statuses_cleanup_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/activitypub/fetch_remote_status_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/activitypub/process_account_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/activitypub/process_status_update_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/fetch_link_card_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/fetch_oembed_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/fetch_remote_status_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/follow_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/import_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/notify_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/process_mentions_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/reblog_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/report_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/resolve_account_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/resolve_url_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/search_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/unallow_domain_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/services/verify_link_service_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/validators/disallowed_hashtags_validator_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/validators/email_mx_validator_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/validators/follow_limit_validator_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/validators/poll_validator_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/validators/status_pin_validator_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/validators/unreserved_username_validator_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/validators/url_validator_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/workers/move_worker_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
# Configuration parameters: SkipBlocks, EnforcedStyle.
 | 
					# Configuration parameters: SkipBlocks, EnforcedStyle.
 | 
				
			||||||
# SupportedStyles: described_class, explicit
 | 
					# SupportedStyles: described_class, explicit
 | 
				
			||||||
| 
						 | 
					@ -701,7 +493,6 @@ RSpec/InstanceVariable:
 | 
				
			||||||
    - 'spec/controllers/statuses_cleanup_controller_spec.rb'
 | 
					    - 'spec/controllers/statuses_cleanup_controller_spec.rb'
 | 
				
			||||||
    - 'spec/models/concerns/account_finder_concern_spec.rb'
 | 
					    - 'spec/models/concerns/account_finder_concern_spec.rb'
 | 
				
			||||||
    - 'spec/models/concerns/account_interactions_spec.rb'
 | 
					    - 'spec/models/concerns/account_interactions_spec.rb'
 | 
				
			||||||
    - 'spec/models/concerns/remotable_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/public_feed_spec.rb'
 | 
					    - 'spec/models/public_feed_spec.rb'
 | 
				
			||||||
    - 'spec/serializers/activitypub/note_serializer_spec.rb'
 | 
					    - 'spec/serializers/activitypub/note_serializer_spec.rb'
 | 
				
			||||||
    - 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
 | 
					    - 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
 | 
				
			||||||
| 
						 | 
					@ -709,17 +500,6 @@ RSpec/InstanceVariable:
 | 
				
			||||||
    - 'spec/services/search_service_spec.rb'
 | 
					    - 'spec/services/search_service_spec.rb'
 | 
				
			||||||
    - 'spec/services/unblock_domain_service_spec.rb'
 | 
					    - 'spec/services/unblock_domain_service_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RSpec/LeakyConstantDeclaration:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/base_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/application_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/accountable_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/signature_verification_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/activitypub/adapter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/connection_pool/shared_connection_pool_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/lib/connection_pool/shared_timed_stack_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/concerns/remotable_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RSpec/LetSetup:
 | 
					RSpec/LetSetup:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'spec/controllers/admin/accounts_controller_spec.rb'
 | 
					    - 'spec/controllers/admin/accounts_controller_spec.rb'
 | 
				
			||||||
| 
						 | 
					@ -745,6 +525,7 @@ 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/settings/imports_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'
 | 
				
			||||||
| 
						 | 
					@ -759,6 +540,7 @@ RSpec/LetSetup:
 | 
				
			||||||
    - 'spec/services/activitypub/process_collection_service_spec.rb'
 | 
					    - 'spec/services/activitypub/process_collection_service_spec.rb'
 | 
				
			||||||
    - 'spec/services/batched_remove_status_service_spec.rb'
 | 
					    - 'spec/services/batched_remove_status_service_spec.rb'
 | 
				
			||||||
    - 'spec/services/block_domain_service_spec.rb'
 | 
					    - 'spec/services/block_domain_service_spec.rb'
 | 
				
			||||||
 | 
					    - 'spec/services/bulk_import_service_spec.rb'
 | 
				
			||||||
    - 'spec/services/delete_account_service_spec.rb'
 | 
					    - 'spec/services/delete_account_service_spec.rb'
 | 
				
			||||||
    - 'spec/services/import_service_spec.rb'
 | 
					    - 'spec/services/import_service_spec.rb'
 | 
				
			||||||
    - 'spec/services/notify_service_spec.rb'
 | 
					    - 'spec/services/notify_service_spec.rb'
 | 
				
			||||||
| 
						 | 
					@ -831,17 +613,6 @@ RSpec/MultipleExpectations:
 | 
				
			||||||
RSpec/MultipleMemoizedHelpers:
 | 
					RSpec/MultipleMemoizedHelpers:
 | 
				
			||||||
  Max: 21
 | 
					  Max: 21
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					 | 
				
			||||||
RSpec/MultipleSubjects:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/collections_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/outboxes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/web/embeds_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/emojis_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/follower_accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/following_accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration parameters: AllowedGroups.
 | 
					# Configuration parameters: AllowedGroups.
 | 
				
			||||||
RSpec/NestedGroups:
 | 
					RSpec/NestedGroups:
 | 
				
			||||||
  Max: 6
 | 
					  Max: 6
 | 
				
			||||||
| 
						 | 
					@ -867,181 +638,6 @@ RSpec/PredicateMatcher:
 | 
				
			||||||
    - 'spec/models/user_spec.rb'
 | 
					    - 'spec/models/user_spec.rb'
 | 
				
			||||||
    - 'spec/services/post_status_service_spec.rb'
 | 
					    - 'spec/services/post_status_service_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
# Configuration parameters: Inferences.
 | 
					 | 
				
			||||||
RSpec/Rails/InferredSpecType:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'spec/controllers/about_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/collections_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/inboxes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/outboxes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/replies_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/account_moderation_notes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/action_logs_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/base_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/change_emails_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/confirmations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/dashboard_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/disputes/appeals_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/domain_allows_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/domain_blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/email_domain_blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/export_domain_allows_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/export_domain_blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/instances_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/settings/branding_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/tags_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/oembed_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/accounts/pins_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/accounts/search_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/admin/reports_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/announcements/reactions_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/announcements_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/apps_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/bookmarks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/conversations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/custom_emojis_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/domain_blocks_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/emails/confirmations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/endorsements_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/favourites_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/filters_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/follow_requests_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/followed_tags_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/instances/activity_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/instances/peers_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/instances_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/lists_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/markers_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/media_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/mutes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/notifications_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/polls/votes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/polls_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/controllers/api/v1/statuses_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/suggestions_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/tags_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v1/trends/tags_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v2/filters_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/api/v2/search_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/application_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/auth/challenges_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/auth/confirmations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/auth/passwords_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/auth/registrations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/auth/sessions_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/account_controller_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/cache_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/challengable_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/export_controller_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/localized_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/signature_verification_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/user_tracking_concern_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/disputes/appeals_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/disputes/strikes_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/home_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/instance_actors_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/intents_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/oauth/authorizations_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/oauth/tokens_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/settings/imports_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/settings/profiles_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/statuses_cleanup_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/tags_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/well_known/host_meta_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/well_known/nodeinfo_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/well_known/webfinger_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/accounts_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/admin/action_logs_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/flashes_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/formatting_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/home_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/routing_helper_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/mailers/admin_mailer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/mailers/notification_mailer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/mailers/user_mailer_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account/field_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_alias_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_conversation_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_deletion_request_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_domain_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_migration_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_moderation_note_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/account_statuses_cleanup_policy_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/admin/account_action_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/admin/action_log_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/announcement_mute_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/announcement_reaction_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/announcement_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/backup_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/canonical_email_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/conversation_mute_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/conversation_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/custom_emoji_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/custom_filter_keyword_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/custom_filter_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/device_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/domain_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/email_domain_block_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/encrypted_message_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/favourite_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/featured_tag_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/follow_recommendation_suppression_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/follow_request_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/follow_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/home_feed_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/identity_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/import_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/invite_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/list_account_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/list_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/login_activity_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/media_attachment_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/mention_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/mute_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/notification_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/poll_vote_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/preview_card_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/preview_card_trend_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/public_feed_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/relay_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/scheduled_status_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/session_activation_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/setting_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/site_upload_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/status_pin_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/status_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/status_stat_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/status_trend_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/system_key_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/tag_follow_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/unavailable_domain_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/user_invite_request_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/user_role_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/user_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/web/push_subscription_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/web/setting_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/webauthn_credentials_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/models/webhook_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RSpec/RepeatedExample:
 | 
					RSpec/RepeatedExample:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'spec/policies/status_policy_spec.rb'
 | 
					    - 'spec/policies/status_policy_spec.rb'
 | 
				
			||||||
| 
						 | 
					@ -1120,7 +716,6 @@ RSpec/VerifiedDoubles:
 | 
				
			||||||
    - 'spec/controllers/api/web/embeds_controller_spec.rb'
 | 
					    - 'spec/controllers/api/web/embeds_controller_spec.rb'
 | 
				
			||||||
    - 'spec/controllers/auth/sessions_controller_spec.rb'
 | 
					    - 'spec/controllers/auth/sessions_controller_spec.rb'
 | 
				
			||||||
    - 'spec/controllers/disputes/appeals_controller_spec.rb'
 | 
					    - 'spec/controllers/disputes/appeals_controller_spec.rb'
 | 
				
			||||||
    - 'spec/controllers/settings/imports_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/helpers/statuses_helper_spec.rb'
 | 
					    - 'spec/helpers/statuses_helper_spec.rb'
 | 
				
			||||||
    - 'spec/lib/suspicious_sign_in_detector_spec.rb'
 | 
					    - 'spec/lib/suspicious_sign_in_detector_spec.rb'
 | 
				
			||||||
    - 'spec/models/account/field_spec.rb'
 | 
					    - 'spec/models/account/field_spec.rb'
 | 
				
			||||||
| 
						 | 
					@ -1148,19 +743,6 @@ RSpec/VerifiedDoubles:
 | 
				
			||||||
    - 'spec/workers/feed_insert_worker_spec.rb'
 | 
					    - 'spec/workers/feed_insert_worker_spec.rb'
 | 
				
			||||||
    - 'spec/workers/regeneration_worker_spec.rb'
 | 
					    - 'spec/workers/regeneration_worker_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					 | 
				
			||||||
# Configuration parameters: Include.
 | 
					 | 
				
			||||||
# Include: app/models/**/*.rb
 | 
					 | 
				
			||||||
Rails/ActiveRecordCallbacksOrder:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/models/account.rb'
 | 
					 | 
				
			||||||
    - 'app/models/account_conversation.rb'
 | 
					 | 
				
			||||||
    - 'app/models/announcement_reaction.rb'
 | 
					 | 
				
			||||||
    - 'app/models/block.rb'
 | 
					 | 
				
			||||||
    - 'app/models/media_attachment.rb'
 | 
					 | 
				
			||||||
    - 'app/models/session_activation.rb'
 | 
					 | 
				
			||||||
    - 'app/models/status.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
Rails/ApplicationController:
 | 
					Rails/ApplicationController:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
| 
						 | 
					@ -1216,12 +798,6 @@ Rails/CreateTableWithTimestamps:
 | 
				
			||||||
    - 'db/migrate/20220824233535_create_status_trends.rb'
 | 
					    - 'db/migrate/20220824233535_create_status_trends.rb'
 | 
				
			||||||
    - 'db/migrate/20221006061337_create_preview_card_trends.rb'
 | 
					    - 'db/migrate/20221006061337_create_preview_card_trends.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
# Configuration parameters: Severity.
 | 
					 | 
				
			||||||
Rails/DeprecatedActiveModelErrorsMethods:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'lib/mastodon/accounts_cli.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
# Configuration parameters: Severity.
 | 
					# Configuration parameters: Severity.
 | 
				
			||||||
Rails/DuplicateAssociation:
 | 
					Rails/DuplicateAssociation:
 | 
				
			||||||
| 
						 | 
					@ -1235,74 +811,6 @@ Rails/Exit:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'config/boot.rb'
 | 
					    - 'config/boot.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: EnforcedStyle.
 | 
					 | 
				
			||||||
# SupportedStyles: slashes, arguments
 | 
					 | 
				
			||||||
Rails/FilePath:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/lib/themes.rb'
 | 
					 | 
				
			||||||
    - 'app/models/setting.rb'
 | 
					 | 
				
			||||||
    - 'app/validators/reaction_validator.rb'
 | 
					 | 
				
			||||||
    - 'config/environments/test.rb'
 | 
					 | 
				
			||||||
    - 'config/initializers/locale.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20171028221157_add_reblogs_to_follows.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20171107143332_add_memorial_to_accounts.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20171107143624_add_disabled_to_users.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20171109012327_add_moderator_to_accounts.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20180615122121_add_autofollow_to_invites.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20181010141500_add_silent_to_mentions.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20181018205649_add_unread_to_account_conversations.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20181127130500_identity_id_to_bigint.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20181127165847_add_show_replies_to_lists.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20190201012802_add_overwrite_to_imports.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20190306145741_add_lock_version_to_polls.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20190307234537_add_approved_to_users.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20191212003415_increase_backup_size.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20200917192924_add_notify_to_follows.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20211231080958_add_category_to_reports.rb'
 | 
					 | 
				
			||||||
    - 'db/migrate/20220613110834_add_action_to_custom_filters.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220307083603_optimize_null_index_conversations_uri.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060545_optimize_null_index_statuses_in_reply_to_account_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060556_optimize_null_index_statuses_in_reply_to_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060614_optimize_null_index_media_attachments_scheduled_status_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060626_optimize_null_index_media_attachments_shortcode.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060641_optimize_null_index_users_reset_password_token.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060653_optimize_null_index_users_created_by_application_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060706_optimize_null_index_statuses_uri.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060722_optimize_null_index_accounts_moved_to_account_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060740_optimize_null_index_oauth_access_tokens_refresh_token.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060750_optimize_null_index_accounts_url.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060809_optimize_null_index_oauth_access_tokens_resource_owner_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060833_optimize_null_index_announcement_reactions_custom_emoji_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060854_optimize_null_index_appeals_approved_by_account_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060913_optimize_null_index_account_migrations_target_account_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060926_optimize_null_index_appeals_rejected_by_account_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060939_optimize_null_index_list_accounts_follow_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220310060959_optimize_null_index_web_push_subscriptions_access_token_id.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220613110802_remove_whole_word_from_custom_filters.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220613110903_remove_irreversible_from_custom_filters.rb'
 | 
					 | 
				
			||||||
    - 'db/post_migrate/20220617202502_migrate_roles.rb'
 | 
					 | 
				
			||||||
    - 'db/seeds.rb'
 | 
					 | 
				
			||||||
    - 'db/seeds/03_roles.rb'
 | 
					 | 
				
			||||||
    - 'lib/tasks/branding.rake'
 | 
					 | 
				
			||||||
    - 'lib/tasks/emojis.rake'
 | 
					 | 
				
			||||||
    - 'lib/tasks/repo.rake'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/custom_emojis_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/fabricators/custom_emoji_fabricator.rb'
 | 
					 | 
				
			||||||
    - 'spec/fabricators/site_upload_fabricator.rb'
 | 
					 | 
				
			||||||
    - 'spec/rails_helper.rb'
 | 
					 | 
				
			||||||
    - 'spec/spec_helper.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration parameters: Include.
 | 
					# Configuration parameters: Include.
 | 
				
			||||||
# Include: app/models/**/*.rb
 | 
					# Include: app/models/**/*.rb
 | 
				
			||||||
Rails/HasAndBelongsToMany:
 | 
					Rails/HasAndBelongsToMany:
 | 
				
			||||||
| 
						 | 
					@ -1445,12 +953,30 @@ Rails/SkipsModelValidations:
 | 
				
			||||||
    - 'spec/services/follow_service_spec.rb'
 | 
					    - 'spec/services/follow_service_spec.rb'
 | 
				
			||||||
    - 'spec/services/update_account_service_spec.rb'
 | 
					    - 'spec/services/update_account_service_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Rails/TransactionExitStatement:
 | 
					# Configuration parameters: Include.
 | 
				
			||||||
 | 
					# Include: db/**/*.rb
 | 
				
			||||||
 | 
					Rails/ThreeStateBooleanColumn:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/lib/activitypub/activity/announce.rb'
 | 
					    - 'db/migrate/20160325130944_add_admin_to_users.rb'
 | 
				
			||||||
    - 'app/lib/activitypub/activity/create.rb'
 | 
					    - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb'
 | 
				
			||||||
    - 'app/lib/activitypub/activity/delete.rb'
 | 
					    - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb'
 | 
				
			||||||
    - 'app/services/activitypub/process_account_service.rb'
 | 
					    - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20170209184350_add_reply_to_statuses.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20170330163835_create_imports.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20170905165803_add_local_to_statuses.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20171210213213_add_local_only_flag_to_statuses.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20190805123746_add_capabilities_to_tags.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20200309150742_add_forwarded_to_reports.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20210609202149_create_login_activities.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20211031031021_create_preview_card_providers.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20220202200743_add_trendable_to_accounts.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20220202200926_add_trendable_to_statuses.rb'
 | 
				
			||||||
 | 
					    - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configuration parameters: Include.
 | 
					# Configuration parameters: Include.
 | 
				
			||||||
# Include: app/models/**/*.rb
 | 
					# Include: app/models/**/*.rb
 | 
				
			||||||
| 
						 | 
					@ -1519,12 +1045,6 @@ Style/CaseEquality:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'config/initializers/trusted_proxies.rb'
 | 
					    - 'config/initializers/trusted_proxies.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
# Configuration parameters: MinBranchesCount.
 | 
					 | 
				
			||||||
Style/CaseLikeIf:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/controllers/concerns/signature_verification.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
 | 
					# Configuration parameters: AllowedMethods, AllowedPatterns.
 | 
				
			||||||
# AllowedMethods: ==, equal?, eql?
 | 
					# AllowedMethods: ==, equal?, eql?
 | 
				
			||||||
| 
						 | 
					@ -1542,16 +1062,10 @@ Style/CombinableLoops:
 | 
				
			||||||
    - 'app/models/form/custom_emoji_batch.rb'
 | 
					    - 'app/models/form/custom_emoji_batch.rb'
 | 
				
			||||||
    - 'app/models/form/ip_block_batch.rb'
 | 
					    - 'app/models/form/ip_block_batch.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					 | 
				
			||||||
Style/ConcatArrayLiterals:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/lib/feed_manager.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
# Configuration parameters: AllowedVars.
 | 
					# Configuration parameters: AllowedVars.
 | 
				
			||||||
Style/FetchEnvVar:
 | 
					Style/FetchEnvVar:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/helpers/application_helper.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/redis_configuration.rb'
 | 
					    - 'app/lib/redis_configuration.rb'
 | 
				
			||||||
    - 'app/lib/translation_service.rb'
 | 
					    - 'app/lib/translation_service.rb'
 | 
				
			||||||
    - 'config/environments/development.rb'
 | 
					    - 'config/environments/development.rb'
 | 
				
			||||||
| 
						 | 
					@ -2001,7 +1515,6 @@ Style/GuardClause:
 | 
				
			||||||
    - 'app/controllers/auth/passwords_controller.rb'
 | 
					    - 'app/controllers/auth/passwords_controller.rb'
 | 
				
			||||||
    - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb'
 | 
					    - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb'
 | 
				
			||||||
    - 'app/lib/activitypub/activity/block.rb'
 | 
					    - 'app/lib/activitypub/activity/block.rb'
 | 
				
			||||||
    - 'app/lib/connection_pool/shared_connection_pool.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/request.rb'
 | 
					    - 'app/lib/request.rb'
 | 
				
			||||||
    - 'app/lib/request_pool.rb'
 | 
					    - 'app/lib/request_pool.rb'
 | 
				
			||||||
    - 'app/lib/webfinger.rb'
 | 
					    - 'app/lib/webfinger.rb'
 | 
				
			||||||
| 
						 | 
					@ -2036,7 +1549,6 @@ Style/HashAsLastArrayItem:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/controllers/admin/statuses_controller.rb'
 | 
					    - 'app/controllers/admin/statuses_controller.rb'
 | 
				
			||||||
    - 'app/controllers/api/v1/statuses_controller.rb'
 | 
					    - 'app/controllers/api/v1/statuses_controller.rb'
 | 
				
			||||||
    - 'app/models/account.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/account_counters.rb'
 | 
					    - 'app/models/concerns/account_counters.rb'
 | 
				
			||||||
    - 'app/models/concerns/status_threading_concern.rb'
 | 
					    - 'app/models/concerns/status_threading_concern.rb'
 | 
				
			||||||
    - 'app/models/status.rb'
 | 
					    - 'app/models/status.rb'
 | 
				
			||||||
| 
						 | 
					@ -2044,19 +1556,6 @@ Style/HashAsLastArrayItem:
 | 
				
			||||||
    - 'app/services/notify_service.rb'
 | 
					    - 'app/services/notify_service.rb'
 | 
				
			||||||
    - 'db/migrate/20181024224956_migrate_account_conversations.rb'
 | 
					    - 'db/migrate/20181024224956_migrate_account_conversations.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					 | 
				
			||||||
# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
 | 
					 | 
				
			||||||
# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
 | 
					 | 
				
			||||||
# SupportedShorthandSyntax: always, never, either, consistent
 | 
					 | 
				
			||||||
Style/HashSyntax:
 | 
					 | 
				
			||||||
  Exclude:
 | 
					 | 
				
			||||||
    - 'app/helpers/application_helper.rb'
 | 
					 | 
				
			||||||
    - 'app/models/media_attachment.rb'
 | 
					 | 
				
			||||||
    - 'lib/terrapin/multi_pipe_extensions.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/reports/actions_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/admin/statuses_controller_spec.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/concerns/signature_verification_spec.rb'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
					# This cop supports unsafe autocorrection (--autocorrect-all).
 | 
				
			||||||
Style/HashTransformValues:
 | 
					Style/HashTransformValues:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
| 
						 | 
					@ -2074,22 +1573,8 @@ Style/IfUnlessModifier:
 | 
				
			||||||
# Configuration parameters: InverseMethods, InverseBlocks.
 | 
					# Configuration parameters: InverseMethods, InverseBlocks.
 | 
				
			||||||
Style/InverseMethods:
 | 
					Style/InverseMethods:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/controllers/concerns/signature_verification.rb'
 | 
					 | 
				
			||||||
    - 'app/helpers/jsonld_helper.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/activitypub/activity/create.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/activitypub/activity/move.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/feed_manager.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/link_details_extractor.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/attachmentable.rb'
 | 
					 | 
				
			||||||
    - 'app/models/concerns/remotable.rb'
 | 
					 | 
				
			||||||
    - 'app/models/custom_filter.rb'
 | 
					    - 'app/models/custom_filter.rb'
 | 
				
			||||||
    - 'app/models/webhook.rb'
 | 
					 | 
				
			||||||
    - 'app/services/activitypub/process_status_update_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/fetch_link_card_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/search_service.rb'
 | 
					 | 
				
			||||||
    - 'app/services/update_account_service.rb'
 | 
					    - 'app/services/update_account_service.rb'
 | 
				
			||||||
    - 'app/workers/web/push_notification_worker.rb'
 | 
					 | 
				
			||||||
    - 'lib/paperclip/color_extractor.rb'
 | 
					 | 
				
			||||||
    - 'spec/controllers/activitypub/replies_controller_spec.rb'
 | 
					    - 'spec/controllers/activitypub/replies_controller_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
| 
						 | 
					@ -2110,12 +1595,10 @@ Style/MapToHash:
 | 
				
			||||||
# SupportedStyles: literals, strict
 | 
					# SupportedStyles: literals, strict
 | 
				
			||||||
Style/MutableConstant:
 | 
					Style/MutableConstant:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/models/account.rb'
 | 
					 | 
				
			||||||
    - 'app/models/tag.rb'
 | 
					    - 'app/models/tag.rb'
 | 
				
			||||||
    - 'app/services/delete_account_service.rb'
 | 
					    - 'app/services/delete_account_service.rb'
 | 
				
			||||||
    - 'config/initializers/twitter_regex.rb'
 | 
					    - 'config/initializers/twitter_regex.rb'
 | 
				
			||||||
    - 'lib/mastodon/migration_warning.rb'
 | 
					    - 'lib/mastodon/migration_warning.rb'
 | 
				
			||||||
    - 'spec/controllers/api/base_controller_spec.rb'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
Style/NilLambda:
 | 
					Style/NilLambda:
 | 
				
			||||||
| 
						 | 
					@ -2199,7 +1682,6 @@ Style/RedundantRegexpEscape:
 | 
				
			||||||
Style/RegexpLiteral:
 | 
					Style/RegexpLiteral:
 | 
				
			||||||
  Exclude:
 | 
					  Exclude:
 | 
				
			||||||
    - 'app/lib/link_details_extractor.rb'
 | 
					    - 'app/lib/link_details_extractor.rb'
 | 
				
			||||||
    - 'app/lib/permalink_redirector.rb'
 | 
					 | 
				
			||||||
    - 'app/lib/plain_text_formatter.rb'
 | 
					    - 'app/lib/plain_text_formatter.rb'
 | 
				
			||||||
    - 'app/lib/tag_manager.rb'
 | 
					    - 'app/lib/tag_manager.rb'
 | 
				
			||||||
    - 'app/lib/text_formatter.rb'
 | 
					    - 'app/lib/text_formatter.rb'
 | 
				
			||||||
| 
						 | 
					@ -2321,11 +1803,14 @@ Style/TrailingCommaInHashLiteral:
 | 
				
			||||||
    - 'config/environments/test.rb'
 | 
					    - 'config/environments/test.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
# Configuration parameters: WordRegex.
 | 
					# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
 | 
				
			||||||
# SupportedStyles: percent, brackets
 | 
					# SupportedStyles: percent, brackets
 | 
				
			||||||
Style/WordArray:
 | 
					Style/WordArray:
 | 
				
			||||||
  EnforcedStyle: percent
 | 
					  Exclude:
 | 
				
			||||||
  MinSize: 6
 | 
					    - 'app/helpers/languages_helper.rb'
 | 
				
			||||||
 | 
					    - 'config/initializers/cors.rb'
 | 
				
			||||||
 | 
					    - 'spec/controllers/settings/imports_controller_spec.rb'
 | 
				
			||||||
 | 
					    - 'spec/models/form/import_spec.rb'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This cop supports safe autocorrection (--autocorrect).
 | 
					# This cop supports safe autocorrection (--autocorrect).
 | 
				
			||||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
 | 
					# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1
									
								
								Aptfile
								
								
								
								
							
							
						
						
									
										1
									
								
								Aptfile
								
								
								
								
							| 
						 | 
					@ -1,4 +1,5 @@
 | 
				
			||||||
ffmpeg
 | 
					ffmpeg
 | 
				
			||||||
 | 
					libopenblas0-pthread
 | 
				
			||||||
libpq-dev
 | 
					libpq-dev
 | 
				
			||||||
libxdamage1
 | 
					libxdamage1
 | 
				
			||||||
libxfixes3
 | 
					libxfixes3
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,10 @@ RUN apt-get update && \
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FROM node:${NODE_VERSION}
 | 
					FROM node:${NODE_VERSION}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Use those args to specify your own version flags & suffixes
 | 
				
			||||||
 | 
					ARG MASTODON_VERSION_FLAGS=""
 | 
				
			||||||
 | 
					ARG MASTODON_VERSION_SUFFIX=""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ARG UID="991"
 | 
					ARG UID="991"
 | 
				
			||||||
ARG GID="991"
 | 
					ARG GID="991"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -84,7 +88,9 @@ COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon
 | 
				
			||||||
ENV RAILS_ENV="production" \
 | 
					ENV RAILS_ENV="production" \
 | 
				
			||||||
    NODE_ENV="production" \
 | 
					    NODE_ENV="production" \
 | 
				
			||||||
    RAILS_SERVE_STATIC_FILES="true" \
 | 
					    RAILS_SERVE_STATIC_FILES="true" \
 | 
				
			||||||
    BIND="0.0.0.0"
 | 
					    BIND="0.0.0.0" \
 | 
				
			||||||
 | 
					    MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \
 | 
				
			||||||
 | 
					    MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Set the run user
 | 
					# Set the run user
 | 
				
			||||||
USER mastodon
 | 
					USER mastodon
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										6
									
								
								Gemfile
								
								
								
								
							| 
						 | 
					@ -30,10 +30,7 @@ gem 'browser'
 | 
				
			||||||
gem 'charlock_holmes', '~> 0.7.7'
 | 
					gem 'charlock_holmes', '~> 0.7.7'
 | 
				
			||||||
gem 'chewy', '~> 7.3'
 | 
					gem 'chewy', '~> 7.3'
 | 
				
			||||||
gem 'devise', '~> 4.9'
 | 
					gem 'devise', '~> 4.9'
 | 
				
			||||||
# The below `v4.x` branch allows attr_encrypted 4.x, which is required for Rails 7.
 | 
					gem 'devise-two-factor', '~> 4.1'
 | 
				
			||||||
# 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'
 | 
				
			||||||
| 
						 | 
					@ -164,3 +161,4 @@ gem 'hcaptcha', '~> 7.1'
 | 
				
			||||||
gem 'cocoon', '~> 1.2'
 | 
					gem 'cocoon', '~> 1.2'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem 'net-http', '~> 0.3.2'
 | 
					gem 'net-http', '~> 0.3.2'
 | 
				
			||||||
 | 
					gem 'rubyzip', '~> 2.3'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										33
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										33
									
								
								Gemfile.lock
								
								
								
								
							| 
						 | 
					@ -27,18 +27,6 @@ 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:
 | 
				
			||||||
| 
						 | 
					@ -218,6 +206,12 @@ GEM
 | 
				
			||||||
      railties (>= 4.1.0)
 | 
					      railties (>= 4.1.0)
 | 
				
			||||||
      responders
 | 
					      responders
 | 
				
			||||||
      warden (~> 1.2.3)
 | 
					      warden (~> 1.2.3)
 | 
				
			||||||
 | 
					    devise-two-factor (4.1.0)
 | 
				
			||||||
 | 
					      activesupport (< 7.1)
 | 
				
			||||||
 | 
					      attr_encrypted (>= 1.3, < 5, != 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)
 | 
				
			||||||
| 
						 | 
					@ -354,15 +348,15 @@ GEM
 | 
				
			||||||
    ipaddress (0.8.3)
 | 
					    ipaddress (0.8.3)
 | 
				
			||||||
    jmespath (1.6.2)
 | 
					    jmespath (1.6.2)
 | 
				
			||||||
    json (2.6.3)
 | 
					    json (2.6.3)
 | 
				
			||||||
    json-canonicalization (0.3.1)
 | 
					    json-canonicalization (0.3.2)
 | 
				
			||||||
    json-jwt (1.15.3)
 | 
					    json-jwt (1.15.3)
 | 
				
			||||||
      activesupport (>= 4.2)
 | 
					      activesupport (>= 4.2)
 | 
				
			||||||
      aes_key_wrap
 | 
					      aes_key_wrap
 | 
				
			||||||
      bindata
 | 
					      bindata
 | 
				
			||||||
      httpclient
 | 
					      httpclient
 | 
				
			||||||
    json-ld (3.2.4)
 | 
					    json-ld (3.2.5)
 | 
				
			||||||
      htmlentities (~> 4.3)
 | 
					      htmlentities (~> 4.3)
 | 
				
			||||||
      json-canonicalization (~> 0.3)
 | 
					      json-canonicalization (~> 0.3, >= 0.3.2)
 | 
				
			||||||
      link_header (~> 0.0, >= 0.0.8)
 | 
					      link_header (~> 0.0, >= 0.0.8)
 | 
				
			||||||
      multi_json (~> 1.15)
 | 
					      multi_json (~> 1.15)
 | 
				
			||||||
      rack (>= 2.2, < 4)
 | 
					      rack (>= 2.2, < 4)
 | 
				
			||||||
| 
						 | 
					@ -492,7 +486,7 @@ GEM
 | 
				
			||||||
    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.5.2)
 | 
					    pg (1.5.3)
 | 
				
			||||||
    pghero (3.3.3)
 | 
					    pghero (3.3.3)
 | 
				
			||||||
      activerecord (>= 6)
 | 
					      activerecord (>= 6)
 | 
				
			||||||
    pkg-config (1.5.1)
 | 
					    pkg-config (1.5.1)
 | 
				
			||||||
| 
						 | 
					@ -626,7 +620,7 @@ GEM
 | 
				
			||||||
    rubocop-performance (1.17.1)
 | 
					    rubocop-performance (1.17.1)
 | 
				
			||||||
      rubocop (>= 1.7.0, < 2.0)
 | 
					      rubocop (>= 1.7.0, < 2.0)
 | 
				
			||||||
      rubocop-ast (>= 0.4.0)
 | 
					      rubocop-ast (>= 0.4.0)
 | 
				
			||||||
    rubocop-rails (2.18.0)
 | 
					    rubocop-rails (2.19.1)
 | 
				
			||||||
      activesupport (>= 4.2.0)
 | 
					      activesupport (>= 4.2.0)
 | 
				
			||||||
      rack (>= 1.1)
 | 
					      rack (>= 1.1)
 | 
				
			||||||
      rubocop (>= 1.33.0, < 2.0)
 | 
					      rubocop (>= 1.33.0, < 2.0)
 | 
				
			||||||
| 
						 | 
					@ -638,6 +632,7 @@ GEM
 | 
				
			||||||
      nokogiri (>= 1.10.5)
 | 
					      nokogiri (>= 1.10.5)
 | 
				
			||||||
      rexml
 | 
					      rexml
 | 
				
			||||||
    ruby2_keywords (0.0.5)
 | 
					    ruby2_keywords (0.0.5)
 | 
				
			||||||
 | 
					    rubyzip (2.3.2)
 | 
				
			||||||
    rufus-scheduler (3.8.2)
 | 
					    rufus-scheduler (3.8.2)
 | 
				
			||||||
      fugit (~> 1.1, >= 1.1.6)
 | 
					      fugit (~> 1.1, >= 1.1.6)
 | 
				
			||||||
    safety_net_attestation (0.4.0)
 | 
					    safety_net_attestation (0.4.0)
 | 
				
			||||||
| 
						 | 
					@ -777,7 +772,6 @@ 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)
 | 
				
			||||||
| 
						 | 
					@ -799,7 +793,7 @@ DEPENDENCIES
 | 
				
			||||||
  concurrent-ruby
 | 
					  concurrent-ruby
 | 
				
			||||||
  connection_pool
 | 
					  connection_pool
 | 
				
			||||||
  devise (~> 4.9)
 | 
					  devise (~> 4.9)
 | 
				
			||||||
  devise-two-factor!
 | 
					  devise-two-factor (~> 4.1)
 | 
				
			||||||
  devise_pam_authenticatable2 (~> 9.2)
 | 
					  devise_pam_authenticatable2 (~> 9.2)
 | 
				
			||||||
  discard (~> 1.2)
 | 
					  discard (~> 1.2)
 | 
				
			||||||
  doorkeeper (~> 5.6)
 | 
					  doorkeeper (~> 5.6)
 | 
				
			||||||
| 
						 | 
					@ -879,6 +873,7 @@ DEPENDENCIES
 | 
				
			||||||
  rubocop-rails
 | 
					  rubocop-rails
 | 
				
			||||||
  rubocop-rspec
 | 
					  rubocop-rspec
 | 
				
			||||||
  ruby-progressbar (~> 1.13)
 | 
					  ruby-progressbar (~> 1.13)
 | 
				
			||||||
 | 
					  rubyzip (~> 2.3)
 | 
				
			||||||
  sanitize (~> 6.0)
 | 
					  sanitize (~> 6.0)
 | 
				
			||||||
  scenic (~> 1.7)
 | 
					  scenic (~> 1.7)
 | 
				
			||||||
  sidekiq (~> 6.5)
 | 
					  sidekiq (~> 6.5)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@ module Admin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
 | 
					      if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
 | 
				
			||||||
        @domain_block.save
 | 
					        @domain_block.save
 | 
				
			||||||
        flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety
 | 
					        flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe
 | 
				
			||||||
        @domain_block.errors.delete(:domain)
 | 
					        @domain_block.errors.delete(:domain)
 | 
				
			||||||
        render :new
 | 
					        render :new
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,8 @@ class Api::V1::MediaController < Api::BaseController
 | 
				
			||||||
    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer
 | 
					    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer
 | 
				
			||||||
  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
 | 
					  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
 | 
				
			||||||
    render json: file_type_error, status: 422
 | 
					    render json: file_type_error, status: 422
 | 
				
			||||||
  rescue Paperclip::Error
 | 
					  rescue Paperclip::Error => e
 | 
				
			||||||
 | 
					    Rails.logger.error "#{e.class}: #{e.message}"
 | 
				
			||||||
    render json: processing_error, status: 500
 | 
					    render json: processing_error, status: 500
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,8 @@ class Api::V2::MediaController < Api::V1::MediaController
 | 
				
			||||||
    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200
 | 
					    render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200
 | 
				
			||||||
  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
 | 
					  rescue Paperclip::Errors::NotIdentifiedByImageMagickError
 | 
				
			||||||
    render json: file_type_error, status: 422
 | 
					    render json: file_type_error, status: 422
 | 
				
			||||||
  rescue Paperclip::Error
 | 
					  rescue Paperclip::Error => e
 | 
				
			||||||
 | 
					    Rails.logger.error "#{e.class}: #{e.message}"
 | 
				
			||||||
    render json: processing_error, status: 500
 | 
					    render json: processing_error, status: 500
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,7 +60,7 @@ class AuthorizeInteractionsController < ApplicationController
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def uri_param
 | 
					  def uri_param
 | 
				
			||||||
    params[:uri] || params.fetch(:acct, '').gsub(/\Aacct:/, '')
 | 
					    params[:uri] || params.fetch(:acct, '').delete_prefix('acct:')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_body_classes
 | 
					  def set_body_classes
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -180,14 +180,15 @@ module SignatureVerification
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def build_signed_string
 | 
					  def build_signed_string
 | 
				
			||||||
    signed_headers.map do |signed_header|
 | 
					    signed_headers.map do |signed_header|
 | 
				
			||||||
      if signed_header == Request::REQUEST_TARGET
 | 
					      case signed_header
 | 
				
			||||||
 | 
					      when Request::REQUEST_TARGET
 | 
				
			||||||
        "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
 | 
					        "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
 | 
				
			||||||
      elsif signed_header == '(created)'
 | 
					      when '(created)'
 | 
				
			||||||
        raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
 | 
					        raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
 | 
				
			||||||
        raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
 | 
					        raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        "(created): #{signature_params['created']}"
 | 
					        "(created): #{signature_params['created']}"
 | 
				
			||||||
      elsif signed_header == '(expires)'
 | 
					      when '(expires)'
 | 
				
			||||||
        raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
 | 
					        raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019'
 | 
				
			||||||
        raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
 | 
					        raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -244,7 +245,7 @@ module SignatureVerification
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if key_id.start_with?('acct:')
 | 
					    if key_id.start_with?('acct:')
 | 
				
			||||||
      stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) }
 | 
					      stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
 | 
				
			||||||
    elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
 | 
					    elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
 | 
				
			||||||
      account   = ActivityPub::TagManager.instance.uri_to_actor(key_id)
 | 
					      account   = ActivityPub::TagManager.instance.uri_to_actor(key_id)
 | 
				
			||||||
      account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
 | 
					      account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ class IntentsController < ApplicationController
 | 
				
			||||||
    if uri.scheme == 'web+mastodon'
 | 
					    if uri.scheme == 'web+mastodon'
 | 
				
			||||||
      case uri.host
 | 
					      case uri.host
 | 
				
			||||||
      when 'follow'
 | 
					      when 'follow'
 | 
				
			||||||
        return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, ''))
 | 
					        return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:'))
 | 
				
			||||||
      when 'share'
 | 
					      when 'share'
 | 
				
			||||||
        return redirect_to share_path(text: uri.query_values['text'])
 | 
					        return redirect_to share_path(text: uri.query_values['text'])
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ class MediaProxyController < ApplicationController
 | 
				
			||||||
  rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
 | 
					  rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show
 | 
					  def show
 | 
				
			||||||
    with_lock("media_download:#{params[:id]}") do
 | 
					    with_redis_lock("media_download:#{params[:id]}") do
 | 
				
			||||||
      @media_attachment = MediaAttachment.remote.attached.find(params[:id])
 | 
					      @media_attachment = MediaAttachment.remote.attached.find(params[:id])
 | 
				
			||||||
      authorize @media_attachment.status, :show?
 | 
					      authorize @media_attachment.status, :show?
 | 
				
			||||||
      redownload! if @media_attachment.needs_redownload? && !reject_media?
 | 
					      redownload! if @media_attachment.needs_redownload? && !reject_media?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ class Settings::ExportsController < Settings::BaseController
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    backup = nil
 | 
					    backup = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with_lock("backup:#{current_user.id}") do
 | 
					    with_redis_lock("backup:#{current_user.id}") do
 | 
				
			||||||
      authorize :backup, :create?
 | 
					      authorize :backup, :create?
 | 
				
			||||||
      backup = current_user.backups.create!
 | 
					      backup = current_user.backups.create!
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,31 +1,97 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Settings::ImportsController < Settings::BaseController
 | 
					require 'csv'
 | 
				
			||||||
  before_action :set_account
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show
 | 
					class Settings::ImportsController < Settings::BaseController
 | 
				
			||||||
    @import = Import.new
 | 
					  before_action :set_bulk_import, only: [:show, :confirm, :destroy]
 | 
				
			||||||
 | 
					  before_action :set_recent_imports, only: [:index]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TYPE_TO_FILENAME_MAP = {
 | 
				
			||||||
 | 
					    following: 'following_accounts_failures.csv',
 | 
				
			||||||
 | 
					    blocking: 'blocked_accounts_failures.csv',
 | 
				
			||||||
 | 
					    muting: 'muted_accounts_failures.csv',
 | 
				
			||||||
 | 
					    domain_blocking: 'blocked_domains_failures.csv',
 | 
				
			||||||
 | 
					    bookmarks: 'bookmarks_failures.csv',
 | 
				
			||||||
 | 
					  }.freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TYPE_TO_HEADERS_MAP = {
 | 
				
			||||||
 | 
					    following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'],
 | 
				
			||||||
 | 
					    blocking: false,
 | 
				
			||||||
 | 
					    muting: ['Account address', 'Hide notifications'],
 | 
				
			||||||
 | 
					    domain_blocking: false,
 | 
				
			||||||
 | 
					    bookmarks: false,
 | 
				
			||||||
 | 
					  }.freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def index
 | 
				
			||||||
 | 
					    @import = Form::Import.new(current_account: current_account)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show; end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def failures
 | 
				
			||||||
 | 
					    @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    respond_to do |format|
 | 
				
			||||||
 | 
					      format.csv do
 | 
				
			||||||
 | 
					        filename = TYPE_TO_FILENAME_MAP[@bulk_import.type.to_sym]
 | 
				
			||||||
 | 
					        headers = TYPE_TO_HEADERS_MAP[@bulk_import.type.to_sym]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        export_data = CSV.generate(headers: headers, write_headers: true) do |csv|
 | 
				
			||||||
 | 
					          @bulk_import.rows.find_each do |row|
 | 
				
			||||||
 | 
					            case @bulk_import.type.to_sym
 | 
				
			||||||
 | 
					            when :following
 | 
				
			||||||
 | 
					              csv << [row.data['acct'], row.data.fetch('show_reblogs', true), row.data.fetch('notify', false), row.data['languages']&.join(', ')]
 | 
				
			||||||
 | 
					            when :blocking
 | 
				
			||||||
 | 
					              csv << [row.data['acct']]
 | 
				
			||||||
 | 
					            when :muting
 | 
				
			||||||
 | 
					              csv << [row.data['acct'], row.data.fetch('hide_notifications', true)]
 | 
				
			||||||
 | 
					            when :domain_blocking
 | 
				
			||||||
 | 
					              csv << [row.data['domain']]
 | 
				
			||||||
 | 
					            when :bookmarks
 | 
				
			||||||
 | 
					              csv << [row.data['uri']]
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					          end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        send_data export_data, filename: filename
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def confirm
 | 
				
			||||||
 | 
					    @bulk_import.update!(state: :scheduled)
 | 
				
			||||||
 | 
					    BulkImportWorker.perform_async(@bulk_import.id)
 | 
				
			||||||
 | 
					    redirect_to settings_imports_path, notice: I18n.t('imports.success')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def create
 | 
					  def create
 | 
				
			||||||
    @import = Import.new(import_params)
 | 
					    @import = Form::Import.new(import_params.merge(current_account: current_account))
 | 
				
			||||||
    @import.account = @account
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if @import.save
 | 
					    if @import.save
 | 
				
			||||||
      ImportWorker.perform_async(@import.id)
 | 
					      redirect_to settings_import_path(@import.bulk_import.id)
 | 
				
			||||||
      redirect_to settings_import_path, notice: I18n.t('imports.success')
 | 
					 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      render :show
 | 
					      # We need to set recent imports as we are displaying the index again
 | 
				
			||||||
 | 
					      set_recent_imports
 | 
				
			||||||
 | 
					      render :index
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def destroy
 | 
				
			||||||
 | 
					    @bulk_import.destroy!
 | 
				
			||||||
 | 
					    redirect_to settings_imports_path
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_account
 | 
					  def import_params
 | 
				
			||||||
    @account = current_user.account
 | 
					    params.require(:form_import).permit(:data, :type, :mode)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def import_params
 | 
					  def set_bulk_import
 | 
				
			||||||
    params.require(:import).permit(:data, :type, :mode)
 | 
					    @bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_recent_imports
 | 
				
			||||||
 | 
					    @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Settings::Preferences::AppearanceController < Settings::PreferencesController
 | 
					class Settings::Preferences::AppearanceController < Settings::Preferences::BaseController
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def after_update_redirect_path
 | 
					  def after_update_redirect_path
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Settings::PreferencesController < Settings::BaseController
 | 
					class Settings::Preferences::BaseController < Settings::BaseController
 | 
				
			||||||
  def show; end
 | 
					  def show; end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def update
 | 
					  def update
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ class Settings::PreferencesController < Settings::BaseController
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def after_update_redirect_path
 | 
					  def after_update_redirect_path
 | 
				
			||||||
    settings_preferences_path
 | 
					    raise 'Override in controller'
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def user_params
 | 
					  def user_params
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Settings::Preferences::NotificationsController < Settings::PreferencesController
 | 
					class Settings::Preferences::NotificationsController < Settings::Preferences::BaseController
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def after_update_redirect_path
 | 
					  def after_update_redirect_path
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Settings::Preferences::OtherController < Settings::PreferencesController
 | 
					class Settings::Preferences::OtherController < Settings::Preferences::BaseController
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def after_update_redirect_path
 | 
					  def after_update_redirect_path
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,14 @@ module WellKnown
 | 
				
			||||||
    private
 | 
					    private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_account
 | 
					    def set_account
 | 
				
			||||||
      @account = Account.find_local!(username_from_resource)
 | 
					      username = username_from_resource
 | 
				
			||||||
 | 
					      @account = begin
 | 
				
			||||||
 | 
					        if username == Rails.configuration.x.local_domain
 | 
				
			||||||
 | 
					          Account.representative
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					          Account.find_local!(username)
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def username_from_resource
 | 
					    def username_from_resource
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,10 +32,6 @@ module ApplicationHelper
 | 
				
			||||||
    paths.any? { |path| current_page?(path) } ? 'active' : ''
 | 
					    paths.any? { |path| current_page?(path) } ? 'active' : ''
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def active_link_to(label, path, **options)
 | 
					 | 
				
			||||||
    link_to label, path, options.merge(class: active_nav_class(path))
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def show_landing_strip?
 | 
					  def show_landing_strip?
 | 
				
			||||||
    !user_signed_in? && !single_user_mode?
 | 
					    !user_signed_in? && !single_user_mode?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -147,7 +143,7 @@ module ApplicationHelper
 | 
				
			||||||
    if prefers_autoplay?
 | 
					    if prefers_autoplay?
 | 
				
			||||||
      image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
 | 
					      image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      image_tag(custom_emoji.image.url(:static), class: 'emojione custom-emoji', alt: ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static)))
 | 
					      image_tag(custom_emoji.image.url(:static), :class => 'emojione custom-emoji', :alt => ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static)))
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,11 +170,11 @@ module ApplicationHelper
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def storage_host
 | 
					  def storage_host
 | 
				
			||||||
    "https://#{ENV['S3_ALIAS_HOST'].presence || ENV['S3_CLOUDFRONT_HOST']}"
 | 
					    URI::HTTPS.build(host: storage_host_name).to_s
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def storage_host?
 | 
					  def storage_host?
 | 
				
			||||||
    ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present?
 | 
					    storage_host_name.present?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def quote_wrap(text, line_width: 80, break_sequence: "\n")
 | 
					  def quote_wrap(text, line_width: 80, break_sequence: "\n")
 | 
				
			||||||
| 
						 | 
					@ -236,4 +232,10 @@ module ApplicationHelper
 | 
				
			||||||
  def prerender_custom_emojis(html, custom_emojis, other_options = {})
 | 
					  def prerender_custom_emojis(html, custom_emojis, other_options = {})
 | 
				
			||||||
    EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
 | 
					    EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def storage_host_name
 | 
				
			||||||
 | 
					    ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +0,0 @@
 | 
				
			||||||
export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const changeLayout = layout => ({
 | 
					 | 
				
			||||||
  type: APP_LAYOUT_CHANGE,
 | 
					 | 
				
			||||||
  layout,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					import { createAction } from '@reduxjs/toolkit';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChangeLayoutPayload = {
 | 
				
			||||||
 | 
					  layout: 'mobile' | 'single-column' | 'multi-column';
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const changeLayout =
 | 
				
			||||||
 | 
					  createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');
 | 
				
			||||||
| 
						 | 
					@ -84,7 +84,7 @@ const DIGIT_CHARACTERS = [
 | 
				
			||||||
  '~',
 | 
					  '~',
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const decode83 = (str) => {
 | 
					export const decode83 = (str: string) => {
 | 
				
			||||||
  let value = 0;
 | 
					  let value = 0;
 | 
				
			||||||
  let c, digit;
 | 
					  let c, digit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,13 +97,13 @@ export const decode83 = (str) => {
 | 
				
			||||||
  return value;
 | 
					  return value;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const intToRGB = int => ({
 | 
					export const intToRGB = (int: number) => ({
 | 
				
			||||||
  r: Math.max(0, (int >> 16)),
 | 
					  r: Math.max(0, (int >> 16)),
 | 
				
			||||||
  g: Math.max(0, (int >> 8) & 255),
 | 
					  g: Math.max(0, (int >> 8) & 255),
 | 
				
			||||||
  b: Math.max(0, (int & 255)),
 | 
					  b: Math.max(0, (int & 255)),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getAverageFromBlurhash = blurhash => {
 | 
					export const getAverageFromBlurhash = (blurhash: string) => {
 | 
				
			||||||
  if (!blurhash) {
 | 
					  if (!blurhash) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
export default function compareId (id1, id2) {
 | 
					export default function compareId (id1: string, id2: string) {
 | 
				
			||||||
  if (id1 === id2) {
 | 
					  if (id1 === id2) {
 | 
				
			||||||
    return 0;
 | 
					    return 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import * as React from 'react';
 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import { autoPlayGif } from 'flavours/glitch/initial_state';
 | 
					import { autoPlayGif } from 'flavours/glitch/initial_state';
 | 
				
			||||||
import { useHovering } from 'hooks/useHovering';
 | 
					import { useHovering } from 'hooks/useHovering';
 | 
				
			||||||
import type { Account } from 'types/resources';
 | 
					import type { Account } from 'flavours/glitch/types/resources';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Props = {
 | 
					type Props = {
 | 
				
			||||||
  account: Account | undefined;
 | 
					  account: Account | undefined;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,65 +0,0 @@
 | 
				
			||||||
// @ts-check
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { decode } from 'blurhash';
 | 
					 | 
				
			||||||
import React, { useRef, useEffect } from 'react';
 | 
					 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * @typedef BlurhashPropsBase
 | 
					 | 
				
			||||||
 * @property {string?} hash Hash to render
 | 
					 | 
				
			||||||
 * @property {number} width
 | 
					 | 
				
			||||||
 * Width of the blurred region in pixels. Defaults to 32
 | 
					 | 
				
			||||||
 * @property {number} [height]
 | 
					 | 
				
			||||||
 * Height of the blurred region in pixels. Defaults to width
 | 
					 | 
				
			||||||
 * @property {boolean} [dummy]
 | 
					 | 
				
			||||||
 * Whether dummy mode is enabled. If enabled, nothing is rendered
 | 
					 | 
				
			||||||
 * and canvas left untouched
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Component that is used to render blurred of blurhash string
 | 
					 | 
				
			||||||
 * @param {BlurhashProps} param1 Props of the component
 | 
					 | 
				
			||||||
 * @returns {JSX.Element} Canvas which will render blurred region element to embed
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function Blurhash({
 | 
					 | 
				
			||||||
  hash,
 | 
					 | 
				
			||||||
  width = 32,
 | 
					 | 
				
			||||||
  height = width,
 | 
					 | 
				
			||||||
  dummy = false,
 | 
					 | 
				
			||||||
  ...canvasProps
 | 
					 | 
				
			||||||
}) {
 | 
					 | 
				
			||||||
  const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const { current: canvas } = canvasRef;
 | 
					 | 
				
			||||||
    canvas.width = canvas.width; // resets canvas
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (dummy || !hash) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const pixels = decode(hash, width, height);
 | 
					 | 
				
			||||||
      const ctx = canvas.getContext('2d');
 | 
					 | 
				
			||||||
      const imageData = new ImageData(pixels, width, height);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // @ts-expect-error
 | 
					 | 
				
			||||||
      ctx.putImageData(imageData, 0, 0);
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      console.error('Blurhash decoding failure', { err, hash });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [dummy, hash, width, height]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Blurhash.propTypes = {
 | 
					 | 
				
			||||||
  hash: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
  width: PropTypes.number,
 | 
					 | 
				
			||||||
  height: PropTypes.number,
 | 
					 | 
				
			||||||
  dummy: PropTypes.bool,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default React.memo(Blurhash);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					import { decode } from 'blurhash';
 | 
				
			||||||
 | 
					import React, { useRef, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Props = {
 | 
				
			||||||
 | 
					  hash: string;
 | 
				
			||||||
 | 
					  width?: number;
 | 
				
			||||||
 | 
					  height?: number;
 | 
				
			||||||
 | 
					  dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
 | 
				
			||||||
 | 
					  children?: never;
 | 
				
			||||||
 | 
					  [key: string]: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function Blurhash({
 | 
				
			||||||
 | 
					  hash,
 | 
				
			||||||
 | 
					  width = 32,
 | 
				
			||||||
 | 
					  height = width,
 | 
				
			||||||
 | 
					  dummy = false,
 | 
				
			||||||
 | 
					  ...canvasProps
 | 
				
			||||||
 | 
					}: Props) {
 | 
				
			||||||
 | 
					  const canvasRef = useRef<HTMLCanvasElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					    const canvas = canvasRef.current!;
 | 
				
			||||||
 | 
					    // eslint-disable-next-line no-self-assign
 | 
				
			||||||
 | 
					    canvas.width = canvas.width; // resets canvas
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (dummy || !hash) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const pixels = decode(hash, width, height);
 | 
				
			||||||
 | 
					      const ctx = canvas.getContext('2d');
 | 
				
			||||||
 | 
					      const imageData = new ImageData(pixels, width, height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ctx?.putImageData(imageData, 0, 0);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      console.error('Blurhash decoding failure', { err, hash });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [dummy, hash, width, height]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default React.memo(Blurhash);
 | 
				
			||||||
| 
						 | 
					@ -1,35 +1,37 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import Icon from 'flavours/glitch/components/icon';
 | 
					import { Icon } from './icon';
 | 
				
			||||||
import AnimatedNumber from 'flavours/glitch/components/animated_number';
 | 
					import { AnimatedNumber } from './animated_number';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class IconButton extends React.PureComponent {
 | 
					type Props = {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
  static propTypes = {
 | 
					  title: string;
 | 
				
			||||||
    className: PropTypes.string,
 | 
					  icon: string;
 | 
				
			||||||
    title: PropTypes.string.isRequired,
 | 
					  onClick?: React.MouseEventHandler<HTMLButtonElement>;
 | 
				
			||||||
    icon: PropTypes.string.isRequired,
 | 
					  onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
 | 
				
			||||||
    onClick: PropTypes.func,
 | 
					  onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
 | 
				
			||||||
    onMouseDown: PropTypes.func,
 | 
					  onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
 | 
				
			||||||
    onKeyDown: PropTypes.func,
 | 
					  size: number;
 | 
				
			||||||
    onKeyPress: PropTypes.func,
 | 
					  active: boolean;
 | 
				
			||||||
    size: PropTypes.number,
 | 
					  expanded?: boolean;
 | 
				
			||||||
    active: PropTypes.bool,
 | 
					  style?: React.CSSProperties;
 | 
				
			||||||
    expanded: PropTypes.bool,
 | 
					  activeStyle?: React.CSSProperties;
 | 
				
			||||||
    style: PropTypes.object,
 | 
					  disabled: boolean;
 | 
				
			||||||
    activeStyle: PropTypes.object,
 | 
					  inverted?: boolean;
 | 
				
			||||||
    disabled: PropTypes.bool,
 | 
					  animate: boolean;
 | 
				
			||||||
    inverted: PropTypes.bool,
 | 
					  overlay: boolean;
 | 
				
			||||||
    animate: PropTypes.bool,
 | 
					  tabIndex: number;
 | 
				
			||||||
    overlay: PropTypes.bool,
 | 
					  label: string;
 | 
				
			||||||
    tabIndex: PropTypes.number,
 | 
					  counter?: number;
 | 
				
			||||||
    label: PropTypes.string,
 | 
					  obfuscateCount?: boolean;
 | 
				
			||||||
    counter: PropTypes.number,
 | 
					  href?: string;
 | 
				
			||||||
    obfuscateCount: PropTypes.bool,
 | 
					  ariaHidden: boolean;
 | 
				
			||||||
    href: PropTypes.string,
 | 
					}
 | 
				
			||||||
    ariaHidden: PropTypes.bool,
 | 
					type States = {
 | 
				
			||||||
  };
 | 
					  activate: boolean,
 | 
				
			||||||
 | 
					  deactivate: boolean,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default class IconButton extends React.PureComponent<Props, States> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static defaultProps = {
 | 
					  static defaultProps = {
 | 
				
			||||||
    size: 18,
 | 
					    size: 18,
 | 
				
			||||||
| 
						 | 
					@ -46,7 +48,7 @@ export default class IconButton extends React.PureComponent {
 | 
				
			||||||
    deactivate: false,
 | 
					    deactivate: false,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillReceiveProps (nextProps) {
 | 
					  UNSAFE_componentWillReceiveProps (nextProps: Props) {
 | 
				
			||||||
    if (!nextProps.animate) return;
 | 
					    if (!nextProps.animate) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.props.active && !nextProps.active) {
 | 
					    if (this.props.active && !nextProps.active) {
 | 
				
			||||||
| 
						 | 
					@ -56,27 +58,27 @@ export default class IconButton extends React.PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleClick = (e) =>  {
 | 
					  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) =>  {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!this.props.disabled) {
 | 
					    if (!this.props.disabled && this.props.onClick != null) {
 | 
				
			||||||
      this.props.onClick(e);
 | 
					      this.props.onClick(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleKeyPress = (e) => {
 | 
					  handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
 | 
				
			||||||
    if (this.props.onKeyPress && !this.props.disabled) {
 | 
					    if (this.props.onKeyPress && !this.props.disabled) {
 | 
				
			||||||
      this.props.onKeyPress(e);
 | 
					      this.props.onKeyPress(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleMouseDown = (e) => {
 | 
					  handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
 | 
				
			||||||
    if (!this.props.disabled && this.props.onMouseDown) {
 | 
					    if (!this.props.disabled && this.props.onMouseDown) {
 | 
				
			||||||
      this.props.onMouseDown(e);
 | 
					      this.props.onMouseDown(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleKeyDown = (e) => {
 | 
					  handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
 | 
				
			||||||
    if (!this.props.disabled && this.props.onKeyDown) {
 | 
					    if (!this.props.disabled && this.props.onKeyDown) {
 | 
				
			||||||
      this.props.onKeyDown(e);
 | 
					      this.props.onKeyDown(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -89,7 +91,7 @@ export default class IconButton extends React.PureComponent {
 | 
				
			||||||
      containerSize = `${this.props.size * 1.28571429}px`;
 | 
					      containerSize = `${this.props.size * 1.28571429}px`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let style = {
 | 
					    const style = {
 | 
				
			||||||
      fontSize: `${this.props.size}px`,
 | 
					      fontSize: `${this.props.size}px`,
 | 
				
			||||||
      height: containerSize,
 | 
					      height: containerSize,
 | 
				
			||||||
      lineHeight: `${this.props.size}px`,
 | 
					      lineHeight: `${this.props.size}px`,
 | 
				
			||||||
| 
						 | 
					@ -144,7 +146,7 @@ export default class IconButton extends React.PureComponent {
 | 
				
			||||||
      </React.Fragment>
 | 
					      </React.Fragment>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (href && !this.prop) {
 | 
					    if (href != null) {
 | 
				
			||||||
      contents = (
 | 
					      contents = (
 | 
				
			||||||
        <a href={href} target='_blank' rel='noopener noreferrer'>
 | 
					        <a href={href} target='_blank' rel='noopener noreferrer'>
 | 
				
			||||||
          {contents}
 | 
					          {contents}
 | 
				
			||||||
| 
						 | 
					@ -101,12 +101,10 @@ class Item extends React.PureComponent {
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { attachment, lang, index, size, standalone, letterbox, displayWidth, visible } = this.props;
 | 
					    const { attachment, lang, index, size, standalone, letterbox, displayWidth, visible } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let badges = [], thumbnail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let width  = 50;
 | 
					    let width  = 50;
 | 
				
			||||||
    let height = 100;
 | 
					    let height = 100;
 | 
				
			||||||
    let top    = 'auto';
 | 
					 | 
				
			||||||
    let left   = 'auto';
 | 
					 | 
				
			||||||
    let bottom = 'auto';
 | 
					 | 
				
			||||||
    let right  = 'auto';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (size === 1) {
 | 
					    if (size === 1) {
 | 
				
			||||||
      width = 100;
 | 
					      width = 100;
 | 
				
			||||||
| 
						 | 
					@ -116,45 +114,13 @@ class Item extends React.PureComponent {
 | 
				
			||||||
      height = 50;
 | 
					      height = 50;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (size === 2) {
 | 
					    if (attachment.get('description')?.length > 0) {
 | 
				
			||||||
      if (index === 0) {
 | 
					      badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
 | 
				
			||||||
        right = '2px';
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        left = '2px';
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    } else if (size === 3) {
 | 
					 | 
				
			||||||
      if (index === 0) {
 | 
					 | 
				
			||||||
        right = '2px';
 | 
					 | 
				
			||||||
      } else if (index > 0) {
 | 
					 | 
				
			||||||
        left = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (index === 1) {
 | 
					 | 
				
			||||||
        bottom = '2px';
 | 
					 | 
				
			||||||
      } else if (index > 1) {
 | 
					 | 
				
			||||||
        top = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (size === 4) {
 | 
					 | 
				
			||||||
      if (index === 0 || index === 2) {
 | 
					 | 
				
			||||||
        right = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (index === 1 || index === 3) {
 | 
					 | 
				
			||||||
        left = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (index < 2) {
 | 
					 | 
				
			||||||
        bottom = '2px';
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        top = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let thumbnail = '';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (attachment.get('type') === 'unknown') {
 | 
					    if (attachment.get('type') === 'unknown') {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
 | 
					        <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
 | 
				
			||||||
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
 | 
					          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
 | 
				
			||||||
            <Blurhash
 | 
					            <Blurhash
 | 
				
			||||||
              hash={attachment.get('blurhash')}
 | 
					              hash={attachment.get('blurhash')}
 | 
				
			||||||
| 
						 | 
					@ -205,6 +171,8 @@ class Item extends React.PureComponent {
 | 
				
			||||||
    } else if (attachment.get('type') === 'gifv') {
 | 
					    } else if (attachment.get('type') === 'gifv') {
 | 
				
			||||||
      const autoPlay = this.getAutoPlay();
 | 
					      const autoPlay = this.getAutoPlay();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      thumbnail = (
 | 
					      thumbnail = (
 | 
				
			||||||
        <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
 | 
					        <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
 | 
				
			||||||
          <video
 | 
					          <video
 | 
				
			||||||
| 
						 | 
					@ -222,14 +190,12 @@ class Item extends React.PureComponent {
 | 
				
			||||||
            loop
 | 
					            loop
 | 
				
			||||||
            muted
 | 
					            muted
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					 | 
				
			||||||
          <span className='media-gallery__gifv__label'>GIF</span>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
 | 
					      <div className={classNames('media-gallery__item', { standalone, letterbox, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
 | 
				
			||||||
        <Blurhash
 | 
					        <Blurhash
 | 
				
			||||||
          hash={attachment.get('blurhash')}
 | 
					          hash={attachment.get('blurhash')}
 | 
				
			||||||
          dummy={!useBlurhash}
 | 
					          dummy={!useBlurhash}
 | 
				
			||||||
| 
						 | 
					@ -237,7 +203,14 @@ class Item extends React.PureComponent {
 | 
				
			||||||
            'media-gallery__preview--hidden': visible && this.state.loaded,
 | 
					            'media-gallery__preview--hidden': visible && this.state.loaded,
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {visible && thumbnail}
 | 
					        {visible && thumbnail}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {badges && (
 | 
				
			||||||
 | 
					          <div className='media-gallery__item__badges'>
 | 
				
			||||||
 | 
					            {badges}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -358,12 +331,10 @@ class MediaGallery extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const computedClass = classNames('media-gallery', { 'full-width': fullwidth });
 | 
					    const computedClass = classNames('media-gallery', { 'full-width': fullwidth });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.isStandaloneEligible() && width) {
 | 
					    if (this.isStandaloneEligible()) { // TODO: cropImages setting
 | 
				
			||||||
      style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
 | 
					      style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
 | 
				
			||||||
    } else if (width) {
 | 
					 | 
				
			||||||
      style.height = width / (16/9);
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return (<div className={computedClass} ref={this.handleRef} />);
 | 
					      style.aspectRatio = '16 / 9';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.isStandaloneEligible()) {
 | 
					    if (this.isStandaloneEligible()) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,62 +3,22 @@ import PropTypes from 'prop-types';
 | 
				
			||||||
import Icon from 'flavours/glitch/components/icon';
 | 
					import Icon from 'flavours/glitch/components/icon';
 | 
				
			||||||
import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
 | 
					import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import { debounce } from 'lodash';
 | 
					 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PictureInPicturePlaceholder extends React.PureComponent {
 | 
					class PictureInPicturePlaceholder extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
    width: PropTypes.number,
 | 
					 | 
				
			||||||
    dispatch: PropTypes.func.isRequired,
 | 
					    dispatch: PropTypes.func.isRequired,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    width: this.props.width,
 | 
					 | 
				
			||||||
    height: this.props.width && (this.props.width / (16/9)),
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleClick = () => {
 | 
					  handleClick = () => {
 | 
				
			||||||
    const { dispatch } = this.props;
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
    dispatch(removePictureInPicture());
 | 
					    dispatch(removePictureInPicture());
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setRef = c => {
 | 
					 | 
				
			||||||
    this.node = c;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.node) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _setDimensions () {
 | 
					 | 
				
			||||||
    const width  = this.node.offsetWidth;
 | 
					 | 
				
			||||||
    const height = width / (16/9);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ width, height });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentDidMount () {
 | 
					 | 
				
			||||||
    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentWillUnmount () {
 | 
					 | 
				
			||||||
    window.removeEventListener('resize', this.handleResize);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleResize = debounce(() => {
 | 
					 | 
				
			||||||
    if (this.node) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, 250, {
 | 
					 | 
				
			||||||
    trailing: true,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { height } = this.state;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex={0} onClick={this.handleClick}>
 | 
					      <div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}>
 | 
				
			||||||
        <Icon id='window-restore' />
 | 
					        <Icon id='window-restore' />
 | 
				
			||||||
        <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
 | 
					        <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -624,7 +624,7 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
    attachments = status.get('media_attachments');
 | 
					    attachments = status.get('media_attachments');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (pictureInPicture.get('inUse')) {
 | 
					    if (pictureInPicture.get('inUse')) {
 | 
				
			||||||
      media.push(<PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />);
 | 
					      media.push(<PictureInPicturePlaceholder />);
 | 
				
			||||||
      mediaIcons.push('video-camera');
 | 
					      mediaIcons.push('video-camera');
 | 
				
			||||||
    } else if (attachments.size > 0) {
 | 
					    } else if (attachments.size > 0) {
 | 
				
			||||||
      if (muted || attachments.some(item => item.get('type') === 'unknown')) {
 | 
					      if (muted || attachments.some(item => item.get('type') === 'unknown')) {
 | 
				
			||||||
| 
						 | 
					@ -680,8 +680,6 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
              fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
 | 
					              fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
 | 
				
			||||||
              preventPlayback={isCollapsed || !isExpanded}
 | 
					              preventPlayback={isCollapsed || !isExpanded}
 | 
				
			||||||
              onOpenVideo={this.handleOpenVideo}
 | 
					              onOpenVideo={this.handleOpenVideo}
 | 
				
			||||||
              width={this.props.cachedMediaWidth}
 | 
					 | 
				
			||||||
              cacheWidth={this.props.cacheMediaWidth}
 | 
					 | 
				
			||||||
              deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
 | 
					              deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
 | 
				
			||||||
              visible={this.state.showMedia}
 | 
					              visible={this.state.showMedia}
 | 
				
			||||||
              onToggleVisibility={this.handleToggleMediaVisibility}
 | 
					              onToggleVisibility={this.handleToggleMediaVisibility}
 | 
				
			||||||
| 
						 | 
					@ -721,8 +719,6 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
          onOpenMedia={this.handleOpenMedia}
 | 
					          onOpenMedia={this.handleOpenMedia}
 | 
				
			||||||
          card={status.get('card')}
 | 
					          card={status.get('card')}
 | 
				
			||||||
          compact
 | 
					          compact
 | 
				
			||||||
          cacheWidth={this.props.cacheMediaWidth}
 | 
					 | 
				
			||||||
          defaultWidth={this.props.cachedMediaWidth}
 | 
					 | 
				
			||||||
          sensitive={status.get('sensitive')}
 | 
					          sensitive={status.get('sensitive')}
 | 
				
			||||||
        />,
 | 
					        />,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { Provider } from 'react-redux';
 | 
					import { Provider } from 'react-redux';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import configureStore from 'flavours/glitch/store/configureStore';
 | 
					import { store } from 'flavours/glitch/store/configureStore';
 | 
				
			||||||
import { hydrateStore } from 'flavours/glitch/actions/store';
 | 
					import { hydrateStore } from 'flavours/glitch/actions/store';
 | 
				
			||||||
import { IntlProvider, addLocaleData } from 'react-intl';
 | 
					import { IntlProvider, addLocaleData } from 'react-intl';
 | 
				
			||||||
import { getLocale } from 'mastodon/locales';
 | 
					import { getLocale } from 'mastodon/locales';
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,6 @@ import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
 | 
				
			||||||
const { localeData, messages } = getLocale();
 | 
					const { localeData, messages } = getLocale();
 | 
				
			||||||
addLocaleData(localeData);
 | 
					addLocaleData(localeData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const store = configureStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if (initialState) {
 | 
					if (initialState) {
 | 
				
			||||||
  store.dispatch(hydrateStore(initialState));
 | 
					  store.dispatch(hydrateStore(initialState));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
 | 
				
			||||||
import { Provider as ReduxProvider } from 'react-redux';
 | 
					import { Provider as ReduxProvider } from 'react-redux';
 | 
				
			||||||
import { BrowserRouter, Route } from 'react-router-dom';
 | 
					import { BrowserRouter, Route } from 'react-router-dom';
 | 
				
			||||||
import { ScrollContext } from 'react-router-scroll-4';
 | 
					import { ScrollContext } from 'react-router-scroll-4';
 | 
				
			||||||
import configureStore from 'flavours/glitch/store/configureStore';
 | 
					import { store } from 'flavours/glitch/store/configureStore';
 | 
				
			||||||
import UI from 'flavours/glitch/features/ui';
 | 
					import UI from 'flavours/glitch/features/ui';
 | 
				
			||||||
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
 | 
					import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
 | 
				
			||||||
import { hydrateStore } from 'flavours/glitch/actions/store';
 | 
					import { hydrateStore } from 'flavours/glitch/actions/store';
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,6 @@ addLocaleData(localeData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
 | 
					const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const store = configureStore();
 | 
					 | 
				
			||||||
const hydrateAction = hydrateStore(initialState);
 | 
					const hydrateAction = hydrateStore(initialState);
 | 
				
			||||||
store.dispatch(hydrateAction);
 | 
					store.dispatch(hydrateAction);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -390,7 +390,7 @@ class Audio extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _getRadius () {
 | 
					  _getRadius () {
 | 
				
			||||||
    return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2);
 | 
					    return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _getScaleCoefficient () {
 | 
					  _getScaleCoefficient () {
 | 
				
			||||||
| 
						 | 
					@ -402,7 +402,7 @@ class Audio extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _getCY() {
 | 
					  _getCY() {
 | 
				
			||||||
    return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient()));
 | 
					    return Math.floor((this.state.height || this.props.height) / 2);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _getAccentColor () {
 | 
					  _getAccentColor () {
 | 
				
			||||||
| 
						 | 
					@ -476,7 +476,7 @@ class Audio extends React.PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
 | 
					      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Blurhash
 | 
					        <Blurhash
 | 
				
			||||||
          hash={blurhash}
 | 
					          hash={blurhash}
 | 
				
			||||||
| 
						 | 
					@ -521,9 +521,16 @@ class Audio extends React.PureComponent {
 | 
				
			||||||
        {(revealed || editable) && <img
 | 
					        {(revealed || editable) && <img
 | 
				
			||||||
          src={this.props.poster}
 | 
					          src={this.props.poster}
 | 
				
			||||||
          alt=''
 | 
					          alt=''
 | 
				
			||||||
          width={(this._getRadius() - TICK_SIZE) * 2}
 | 
					          style={{
 | 
				
			||||||
          height={(this._getRadius() - TICK_SIZE) * 2}
 | 
					            position: 'absolute',
 | 
				
			||||||
          style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }}
 | 
					            left: '50%',
 | 
				
			||||||
 | 
					            top: '50%',
 | 
				
			||||||
 | 
					            height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`,
 | 
				
			||||||
 | 
					            aspectRatio: '1',
 | 
				
			||||||
 | 
					            transform: 'translate(-50%, -50%)',
 | 
				
			||||||
 | 
					            borderRadius: '50%',
 | 
				
			||||||
 | 
					            pointerEvents: 'none',
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
        />}
 | 
					        />}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
 | 
					        <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,6 @@ import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
 | 
				
			||||||
import Icon from 'flavours/glitch/components/icon';
 | 
					import Icon from 'flavours/glitch/components/icon';
 | 
				
			||||||
import { useBlurhash } from 'flavours/glitch/initial_state';
 | 
					import { useBlurhash } from 'flavours/glitch/initial_state';
 | 
				
			||||||
import Blurhash from 'flavours/glitch/components/blurhash';
 | 
					import Blurhash from 'flavours/glitch/components/blurhash';
 | 
				
			||||||
import { debounce } from 'lodash';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getHostname = url => {
 | 
					const getHostname = url => {
 | 
				
			||||||
  const parser = document.createElement('a');
 | 
					  const parser = document.createElement('a');
 | 
				
			||||||
| 
						 | 
					@ -45,8 +44,6 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
    card: ImmutablePropTypes.map,
 | 
					    card: ImmutablePropTypes.map,
 | 
				
			||||||
    onOpenMedia: PropTypes.func.isRequired,
 | 
					    onOpenMedia: PropTypes.func.isRequired,
 | 
				
			||||||
    compact: PropTypes.bool,
 | 
					    compact: PropTypes.bool,
 | 
				
			||||||
    defaultWidth: PropTypes.number,
 | 
					 | 
				
			||||||
    cacheWidth: PropTypes.func,
 | 
					 | 
				
			||||||
    sensitive: PropTypes.bool,
 | 
					    sensitive: PropTypes.bool,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,7 +52,6 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = {
 | 
					  state = {
 | 
				
			||||||
    width: this.props.defaultWidth || 280,
 | 
					 | 
				
			||||||
    previewLoaded: false,
 | 
					    previewLoaded: false,
 | 
				
			||||||
    embedded: false,
 | 
					    embedded: false,
 | 
				
			||||||
    revealed: !this.props.sensitive,
 | 
					    revealed: !this.props.sensitive,
 | 
				
			||||||
| 
						 | 
					@ -78,24 +74,6 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
    window.removeEventListener('resize', this.handleResize);
 | 
					    window.removeEventListener('resize', this.handleResize);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _setDimensions () {
 | 
					 | 
				
			||||||
    const width = this.node.offsetWidth;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.props.cacheWidth) {
 | 
					 | 
				
			||||||
      this.props.cacheWidth(width);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ width });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleResize = debounce(() => {
 | 
					 | 
				
			||||||
    if (this.node) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, 250, {
 | 
					 | 
				
			||||||
    trailing: true,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handlePhotoClick = () => {
 | 
					  handlePhotoClick = () => {
 | 
				
			||||||
    const { card, onOpenMedia } = this.props;
 | 
					    const { card, onOpenMedia } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -129,10 +107,6 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setRef = c => {
 | 
					  setRef = c => {
 | 
				
			||||||
    this.node = c;
 | 
					    this.node = c;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.node) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleImageLoad = () => {
 | 
					  handleImageLoad = () => {
 | 
				
			||||||
| 
						 | 
					@ -148,36 +122,31 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
  renderVideo () {
 | 
					  renderVideo () {
 | 
				
			||||||
    const { card }  = this.props;
 | 
					    const { card }  = this.props;
 | 
				
			||||||
    const content   = { __html: addAutoPlay(card.get('html')) };
 | 
					    const content   = { __html: addAutoPlay(card.get('html')) };
 | 
				
			||||||
    const { width } = this.state;
 | 
					 | 
				
			||||||
    const ratio     = card.get('width') / card.get('height');
 | 
					 | 
				
			||||||
    const height    = width / ratio;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        ref={this.setRef}
 | 
					        ref={this.setRef}
 | 
				
			||||||
        className='status-card__image status-card-video'
 | 
					        className='status-card__image status-card-video'
 | 
				
			||||||
        dangerouslySetInnerHTML={content}
 | 
					        dangerouslySetInnerHTML={content}
 | 
				
			||||||
        style={{ height }}
 | 
					        style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { card, compact } = this.props;
 | 
					    const { card, compact } = this.props;
 | 
				
			||||||
    const { width, embedded, revealed } = this.state;
 | 
					    const { embedded, revealed } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (card === null) {
 | 
					    if (card === null) {
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
 | 
					    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
 | 
				
			||||||
    const horizontal  = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
 | 
					    const horizontal  = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded;
 | 
				
			||||||
    const interactive = card.get('type') !== 'link';
 | 
					    const interactive = card.get('type') !== 'link';
 | 
				
			||||||
    const className   = classnames('status-card', { horizontal, compact, interactive });
 | 
					    const className   = classnames('status-card', { horizontal, compact, interactive });
 | 
				
			||||||
    const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
 | 
					    const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
 | 
				
			||||||
    const language    = card.get('language') || '';
 | 
					    const language    = card.get('language') || '';
 | 
				
			||||||
    const ratio       = card.get('width') / card.get('height');
 | 
					 | 
				
			||||||
    const height      = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const description = (
 | 
					    const description = (
 | 
				
			||||||
      <div className='status-card__content' lang={language}>
 | 
					      <div className='status-card__content' lang={language}>
 | 
				
			||||||
| 
						 | 
					@ -187,6 +156,14 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const thumbnailStyle = {
 | 
				
			||||||
 | 
					      visibility: revealed? null : 'hidden',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (horizontal) {
 | 
				
			||||||
 | 
					      thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let embed     = '';
 | 
					    let embed     = '';
 | 
				
			||||||
    let canvas = (
 | 
					    let canvas = (
 | 
				
			||||||
      <Blurhash
 | 
					      <Blurhash
 | 
				
			||||||
| 
						 | 
					@ -197,7 +174,7 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
        dummy={!useBlurhash}
 | 
					        dummy={!useBlurhash}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
 | 
					    let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
 | 
				
			||||||
    let spoilerButton = (
 | 
					    let spoilerButton = (
 | 
				
			||||||
      <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
 | 
					      <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
 | 
				
			||||||
        <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
					        <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -378,7 +378,7 @@ class UI extends React.Component {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (layout !== this.props.layout) {
 | 
					    if (layout !== this.props.layout) {
 | 
				
			||||||
      this.handleLayoutChange.cancel();
 | 
					      this.handleLayoutChange.cancel();
 | 
				
			||||||
      this.props.dispatch(changeLayout(layout));
 | 
					      this.props.dispatch(changeLayout({ layout }));
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.handleLayoutChange();
 | 
					      this.handleLayoutChange();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import { is } from 'immutable';
 | 
					import { is } from 'immutable';
 | 
				
			||||||
import { throttle, debounce } from 'lodash';
 | 
					import { throttle } from 'lodash';
 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
 | 
					import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
 | 
				
			||||||
import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
 | 
					import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
 | 
				
			||||||
| 
						 | 
					@ -102,8 +102,6 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    src: PropTypes.string.isRequired,
 | 
					    src: PropTypes.string.isRequired,
 | 
				
			||||||
    alt: PropTypes.string,
 | 
					    alt: PropTypes.string,
 | 
				
			||||||
    lang: PropTypes.string,
 | 
					    lang: PropTypes.string,
 | 
				
			||||||
    width: PropTypes.number,
 | 
					 | 
				
			||||||
    height: PropTypes.number,
 | 
					 | 
				
			||||||
    sensitive: PropTypes.bool,
 | 
					    sensitive: PropTypes.bool,
 | 
				
			||||||
    currentTime: PropTypes.number,
 | 
					    currentTime: PropTypes.number,
 | 
				
			||||||
    onOpenVideo: PropTypes.func,
 | 
					    onOpenVideo: PropTypes.func,
 | 
				
			||||||
| 
						 | 
					@ -112,7 +110,6 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    inline: PropTypes.bool,
 | 
					    inline: PropTypes.bool,
 | 
				
			||||||
    editable: PropTypes.bool,
 | 
					    editable: PropTypes.bool,
 | 
				
			||||||
    alwaysVisible: PropTypes.bool,
 | 
					    alwaysVisible: PropTypes.bool,
 | 
				
			||||||
    cacheWidth: PropTypes.func,
 | 
					 | 
				
			||||||
    visible: PropTypes.bool,
 | 
					    visible: PropTypes.bool,
 | 
				
			||||||
    letterbox: PropTypes.bool,
 | 
					    letterbox: PropTypes.bool,
 | 
				
			||||||
    fullwidth: PropTypes.bool,
 | 
					    fullwidth: PropTypes.bool,
 | 
				
			||||||
| 
						 | 
					@ -138,41 +135,16 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    volume: 0.5,
 | 
					    volume: 0.5,
 | 
				
			||||||
    paused: true,
 | 
					    paused: true,
 | 
				
			||||||
    dragging: false,
 | 
					    dragging: false,
 | 
				
			||||||
    containerWidth: this.props.width,
 | 
					 | 
				
			||||||
    fullscreen: false,
 | 
					    fullscreen: false,
 | 
				
			||||||
    hovered: false,
 | 
					    hovered: false,
 | 
				
			||||||
    muted: false,
 | 
					    muted: false,
 | 
				
			||||||
    revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
 | 
					    revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillReceiveProps (nextProps) {
 | 
					 | 
				
			||||||
    if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
 | 
					 | 
				
			||||||
      this.setState({ revealed: nextProps.visible });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setPlayerRef = c => {
 | 
					  setPlayerRef = c => {
 | 
				
			||||||
    this.player = c;
 | 
					    this.player = c;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.player) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _setDimensions () {
 | 
					 | 
				
			||||||
    const width = this.player.offsetWidth;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (width && width !== this.state.containerWidth) {
 | 
					 | 
				
			||||||
      if (this.props.cacheWidth) {
 | 
					 | 
				
			||||||
        this.props.cacheWidth(width);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this.setState({
 | 
					 | 
				
			||||||
        containerWidth: width,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setVideoRef = c => {
 | 
					  setVideoRef = c => {
 | 
				
			||||||
    this.video = c;
 | 
					    this.video = c;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -381,12 +353,10 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
 | 
					    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    window.addEventListener('scroll', this.handleScroll);
 | 
					    window.addEventListener('scroll', this.handleScroll);
 | 
				
			||||||
    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillUnmount () {
 | 
					  componentWillUnmount () {
 | 
				
			||||||
    window.removeEventListener('scroll', this.handleScroll);
 | 
					    window.removeEventListener('scroll', this.handleScroll);
 | 
				
			||||||
    window.removeEventListener('resize', this.handleResize);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
 | 
					    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
 | 
				
			||||||
    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
 | 
					    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
 | 
				
			||||||
| 
						 | 
					@ -403,26 +373,18 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidUpdate (prevProps) {
 | 
					  componentWillReceiveProps (nextProps) {
 | 
				
			||||||
    if (this.player && this.player.offsetWidth && this.player.offsetWidth !== this.state.containerWidth && !this.state.fullscreen) {
 | 
					    if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) {
 | 
				
			||||||
      if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth);
 | 
					      this.setState({ revealed: nextProps.visible });
 | 
				
			||||||
      this.setState({
 | 
					 | 
				
			||||||
        containerWidth: this.player.offsetWidth,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentDidUpdate (prevProps) {
 | 
				
			||||||
    if (this.video && this.state.revealed && this.props.preventPlayback && !prevProps.preventPlayback) {
 | 
					    if (this.video && this.state.revealed && this.props.preventPlayback && !prevProps.preventPlayback) {
 | 
				
			||||||
      this.video.pause();
 | 
					      this.video.pause();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleResize = debounce(() => {
 | 
					 | 
				
			||||||
    if (this.player) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, 250, {
 | 
					 | 
				
			||||||
    trailing: true,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleScroll = throttle(() => {
 | 
					  handleScroll = throttle(() => {
 | 
				
			||||||
    if (!this.video) {
 | 
					    if (!this.video) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
| 
						 | 
					@ -540,21 +502,12 @@ class Video extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
 | 
					    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
 | 
				
			||||||
    const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
 | 
					    const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
 | 
				
			||||||
    const progress = Math.min((currentTime / duration) * 100, 100);
 | 
					    const progress = Math.min((currentTime / duration) * 100, 100);
 | 
				
			||||||
    const playerStyle = {};
 | 
					    const playerStyle = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth });
 | 
					    if (inline) {
 | 
				
			||||||
 | 
					      playerStyle.aspectRatio = '16 / 9';
 | 
				
			||||||
    let { width, height } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (inline && containerWidth) {
 | 
					 | 
				
			||||||
      width  = containerWidth;
 | 
					 | 
				
			||||||
      height = containerWidth / (16/9);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      playerStyle.height = height;
 | 
					 | 
				
			||||||
    } else if (inline) {
 | 
					 | 
				
			||||||
      return (<div className={computedClass} ref={this.setPlayerRef} tabIndex={0} />);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let preload;
 | 
					    let preload;
 | 
				
			||||||
| 
						 | 
					@ -578,7 +531,7 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        role='menuitem'
 | 
					        role='menuitem'
 | 
				
			||||||
        className={computedClass}
 | 
					        className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, editable, letterbox, 'full-width': fullwidth })}
 | 
				
			||||||
        style={playerStyle}
 | 
					        style={playerStyle}
 | 
				
			||||||
        ref={this.setPlayerRef}
 | 
					        ref={this.setPlayerRef}
 | 
				
			||||||
        onMouseEnter={this.handleMouseEnter}
 | 
					        onMouseEnter={this.handleMouseEnter}
 | 
				
			||||||
| 
						 | 
					@ -605,8 +558,6 @@ class Video extends React.PureComponent {
 | 
				
			||||||
          aria-label={alt}
 | 
					          aria-label={alt}
 | 
				
			||||||
          title={alt}
 | 
					          title={alt}
 | 
				
			||||||
          lang={lang}
 | 
					          lang={lang}
 | 
				
			||||||
          width={width}
 | 
					 | 
				
			||||||
          height={height}
 | 
					 | 
				
			||||||
          volume={volume}
 | 
					          volume={volume}
 | 
				
			||||||
          onClick={this.togglePlay}
 | 
					          onClick={this.togglePlay}
 | 
				
			||||||
          onKeyDown={this.handleVideoKeyDown}
 | 
					          onKeyDown={this.handleVideoKeyDown}
 | 
				
			||||||
| 
						 | 
					@ -615,6 +566,7 @@ class Video extends React.PureComponent {
 | 
				
			||||||
          onLoadedData={this.handleLoadedData}
 | 
					          onLoadedData={this.handleLoadedData}
 | 
				
			||||||
          onProgress={this.handleProgress}
 | 
					          onProgress={this.handleProgress}
 | 
				
			||||||
          onVolumeChange={this.handleVolumeChange}
 | 
					          onVolumeChange={this.handleVolumeChange}
 | 
				
			||||||
 | 
					          style={{ ...playerStyle, width: '100%' }}
 | 
				
			||||||
        />}
 | 
					        />}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
 | 
					        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,21 +1,12 @@
 | 
				
			||||||
// @ts-check
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { supportsPassiveEvents } from 'detect-passive-events';
 | 
					import { supportsPassiveEvents } from 'detect-passive-events';
 | 
				
			||||||
import { forceSingleColumn } from 'flavours/glitch/initial_state';
 | 
					import { forceSingleColumn } from 'flavours/glitch/initial_state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LAYOUT_BREAKPOINT = 630;
 | 
					const LAYOUT_BREAKPOINT = 630;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
 | 
				
			||||||
 * @param {number} width
 | 
					 | 
				
			||||||
 * @returns {boolean}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export const isMobile = width => width <= LAYOUT_BREAKPOINT;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
 | 
				
			||||||
 * @param {string} layout_local_setting
 | 
					export const layoutFromWindow = (layout_local_setting : string): LayoutType => {
 | 
				
			||||||
 * @returns {string}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export const layoutFromWindow = (layout_local_setting) => {
 | 
					 | 
				
			||||||
  switch (layout_local_setting) {
 | 
					  switch (layout_local_setting) {
 | 
				
			||||||
  case 'multiple':
 | 
					  case 'multiple':
 | 
				
			||||||
    return 'multi-column';
 | 
					    return 'multi-column';
 | 
				
			||||||
| 
						 | 
					@ -36,8 +27,9 @@ export const layoutFromWindow = (layout_local_setting) => {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
				
			||||||
// @ts-expect-error
 | 
					// @ts-expect-error
 | 
				
			||||||
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
 | 
					const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && window.MSStream != null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 | 
					const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import ReactDOM from 'react-dom';
 | 
					import ReactDOM from 'react-dom';
 | 
				
			||||||
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
 | 
					import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
 | 
				
			||||||
import Mastodon, { store } from 'flavours/glitch/containers/mastodon';
 | 
					import Mastodon from 'flavours/glitch/containers/mastodon';
 | 
				
			||||||
 | 
					import { store } from 'flavours/glitch/store/configureStore';
 | 
				
			||||||
import { me } from 'flavours/glitch/initial_state';
 | 
					import { me } from 'flavours/glitch/initial_state';
 | 
				
			||||||
import ready from 'flavours/glitch/ready';
 | 
					import ready from 'flavours/glitch/ready';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 | 
					import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
 | 
				
			||||||
import { APP_LAYOUT_CHANGE } from 'flavours/glitch/actions/app';
 | 
					import { changeLayout } from 'flavours/glitch/actions/app';
 | 
				
			||||||
import { Map as ImmutableMap } from 'immutable';
 | 
					import { Map as ImmutableMap } from 'immutable';
 | 
				
			||||||
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
 | 
					import { layoutFromWindow } from 'flavours/glitch/is_mobile';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,8 +16,8 @@ export default function meta(state = initialState, action) {
 | 
				
			||||||
    return state.merge(action.state.get('meta'))
 | 
					    return state.merge(action.state.get('meta'))
 | 
				
			||||||
      .set('permissions', action.state.getIn(['role', 'permissions']))
 | 
					      .set('permissions', action.state.getIn(['role', 'permissions']))
 | 
				
			||||||
      .set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout'])));
 | 
					      .set('layout', layoutFromWindow(action.state.getIn(['local_settings', 'layout'])));
 | 
				
			||||||
  case APP_LAYOUT_CHANGE:
 | 
					  case changeLayout.type:
 | 
				
			||||||
    return state.set('layout', action.layout);
 | 
					    return state.set('layout', action.payload.layout);
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
 | 
					const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
 | 
				
			||||||
 | 
					const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => {
 | 
				
			||||||
const scroll = (node, key, target) => {
 | 
					 | 
				
			||||||
  const startTime = Date.now();
 | 
					  const startTime = Date.now();
 | 
				
			||||||
  const offset    = node[key];
 | 
					  const offset    = node[key];
 | 
				
			||||||
  const gap       = target - offset;
 | 
					  const gap       = target - offset;
 | 
				
			||||||
| 
						 | 
					@ -28,5 +27,5 @@ const scroll = (node, key, target) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;
 | 
					const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
 | 
					export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
 | 
				
			||||||
export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
 | 
					export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,16 @@
 | 
				
			||||||
import { createStore, applyMiddleware, compose } from 'redux';
 | 
					import { configureStore } from '@reduxjs/toolkit';
 | 
				
			||||||
import thunk from 'redux-thunk';
 | 
					import thunk from 'redux-thunk';
 | 
				
			||||||
import appReducer from '../reducers';
 | 
					import appReducer from '../reducers';
 | 
				
			||||||
import loadingBarMiddleware from '../middleware/loading_bar';
 | 
					import loadingBarMiddleware from '../middleware/loading_bar';
 | 
				
			||||||
import errorsMiddleware from '../middleware/errors';
 | 
					import errorsMiddleware from '../middleware/errors';
 | 
				
			||||||
import soundsMiddleware from '../middleware/sounds';
 | 
					import soundsMiddleware from '../middleware/sounds';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function configureStore() {
 | 
					export const store = configureStore({
 | 
				
			||||||
  return createStore(appReducer, compose(applyMiddleware(
 | 
					  reducer: appReducer,
 | 
				
			||||||
 | 
					  middleware: [
 | 
				
			||||||
    thunk,
 | 
					    thunk,
 | 
				
			||||||
    loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
 | 
					    loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
 | 
				
			||||||
    errorsMiddleware(),
 | 
					    errorsMiddleware(),
 | 
				
			||||||
    soundsMiddleware(),
 | 
					    soundsMiddleware(),
 | 
				
			||||||
  ), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f));
 | 
					  ],
 | 
				
			||||||
}
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,6 @@
 | 
				
			||||||
    margin-right: -14px;
 | 
					    margin-right: -14px;
 | 
				
			||||||
    width: inherit;
 | 
					    width: inherit;
 | 
				
			||||||
    max-width: none;
 | 
					    max-width: none;
 | 
				
			||||||
    height: 250px;
 | 
					 | 
				
			||||||
    border-radius: 0;
 | 
					    border-radius: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,30 +43,25 @@
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-gallery__gifv__label {
 | 
					.media-gallery__item__badges {
 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  color: $primary-text-color;
 | 
					 | 
				
			||||||
  background: rgba($base-overlay-background, 0.5);
 | 
					 | 
				
			||||||
  bottom: 6px;
 | 
					  bottom: 6px;
 | 
				
			||||||
  inset-inline-start: 6px;
 | 
					  inset-inline-start: 6px;
 | 
				
			||||||
  padding: 2px 6px;
 | 
					  display: flex;
 | 
				
			||||||
  border-radius: 2px;
 | 
					  gap: 2px;
 | 
				
			||||||
  font-size: 11px;
 | 
					 | 
				
			||||||
  font-weight: 600;
 | 
					 | 
				
			||||||
  z-index: 1;
 | 
					 | 
				
			||||||
  pointer-events: none;
 | 
					 | 
				
			||||||
  opacity: 0.9;
 | 
					 | 
				
			||||||
  transition: opacity 0.1s ease;
 | 
					 | 
				
			||||||
  line-height: 18px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-gallery__gifv {
 | 
					.media-gallery__gifv__label {
 | 
				
			||||||
  &:hover {
 | 
					  display: block;
 | 
				
			||||||
    .media-gallery__gifv__label {
 | 
					  color: $white;
 | 
				
			||||||
      opacity: 1;
 | 
					  background: rgba($black, 0.65);
 | 
				
			||||||
    }
 | 
					  padding: 2px 6px;
 | 
				
			||||||
  }
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  font-size: 11px;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  line-height: 18px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-gallery {
 | 
					.media-gallery {
 | 
				
			||||||
| 
						 | 
					@ -77,6 +72,10 @@
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  min-height: 64px;
 | 
					  min-height: 64px;
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: 50% 50%;
 | 
				
			||||||
 | 
					  grid-template-rows: 50% 50%;
 | 
				
			||||||
 | 
					  gap: 2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @include fullwidth-gallery;
 | 
					  @include fullwidth-gallery;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -85,13 +84,16 @@
 | 
				
			||||||
  border: 0;
 | 
					  border: 0;
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  float: left;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .full-width & {
 | 
					  &--tall {
 | 
				
			||||||
    border-radius: 0;
 | 
					    grid-row: span 2;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &--wide {
 | 
				
			||||||
 | 
					    grid-column: span 2;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.standalone {
 | 
					  &.standalone {
 | 
				
			||||||
| 
						 | 
					@ -101,6 +103,10 @@
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .full-width & {
 | 
				
			||||||
 | 
					    border-radius: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.letterbox {
 | 
					  &.letterbox {
 | 
				
			||||||
    background: $base-shadow-color;
 | 
					    background: $base-shadow-color;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -691,7 +691,6 @@ a.status__display-name,
 | 
				
			||||||
  margin-inline-end: 10px;
 | 
					  margin-inline-end: 10px;
 | 
				
			||||||
  height: 48px;
 | 
					  height: 48px;
 | 
				
			||||||
  width: 48px;
 | 
					  width: 48px;
 | 
				
			||||||
  box-shadow: 0 0 0 2px $ui-base-color;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.muted {
 | 
					.muted {
 | 
				
			||||||
| 
						 | 
					@ -809,6 +808,10 @@ a.status-card {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.status-card-video {
 | 
					.status-card-video {
 | 
				
			||||||
 | 
					  // Firefox has a bug where frameborder=0 iframes add some extra blank space
 | 
				
			||||||
 | 
					  // see https://bugzilla.mozilla.org/show_bug.cgi?id=155174
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  iframe {
 | 
					  iframe {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
| 
						 | 
					@ -1154,6 +1157,7 @@ a.status-card.compact:hover {
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  color: $darker-text-color;
 | 
					  color: $darker-text-color;
 | 
				
			||||||
 | 
					  aspect-ratio: 16 / 9;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  i {
 | 
					  i {
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					import type { Record } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AccountValues = {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  avatar: string;
 | 
				
			||||||
 | 
					  avatar_static: string;
 | 
				
			||||||
 | 
					  [key: string]: any;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Account = Record<AccountValues>;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export type ValueOf<T> = T[keyof T];
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
export const decode = base64 => {
 | 
					export const decode = (base64: string): Uint8Array => {
 | 
				
			||||||
  const rawData = window.atob(base64);
 | 
					  const rawData = window.atob(base64);
 | 
				
			||||||
  const outputArray = new Uint8Array(rawData.length);
 | 
					  const outputArray = new Uint8Array(rawData.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
export const toServerSideType = columnType => {
 | 
					export const toServerSideType = (columnType: string) => {
 | 
				
			||||||
  switch (columnType) {
 | 
					  switch (columnType) {
 | 
				
			||||||
  case 'home':
 | 
					  case 'home':
 | 
				
			||||||
  case 'notifications':
 | 
					  case 'notifications':
 | 
				
			||||||
| 
						 | 
					@ -1,23 +1,19 @@
 | 
				
			||||||
// @ts-check
 | 
					import type { ValueOf } from 'flavours/glitch/types/util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DECIMAL_UNITS = Object.freeze({
 | 
					export const DECIMAL_UNITS = Object.freeze({
 | 
				
			||||||
  ONE: 1,
 | 
					  ONE: 1,
 | 
				
			||||||
  TEN: 10,
 | 
					  TEN: 10,
 | 
				
			||||||
  HUNDRED: Math.pow(10, 2),
 | 
					  HUNDRED: 100,
 | 
				
			||||||
  THOUSAND: Math.pow(10, 3),
 | 
					  THOUSAND: 1_000,
 | 
				
			||||||
  MILLION: Math.pow(10, 6),
 | 
					  MILLION: 1_000_000,
 | 
				
			||||||
  BILLION: Math.pow(10, 9),
 | 
					  BILLION: 1_000_000_000,
 | 
				
			||||||
  TRILLION: Math.pow(10, 12),
 | 
					  TRILLION: 1_000_000_000_000,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
 | 
					const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
 | 
				
			||||||
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
					const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * @typedef {[number, number, number]} ShortNumber
 | 
					 | 
				
			||||||
 * Array of: shorten number, unit of shorten number and maximum fraction digits
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {number} sourceNumber Number to convert to short number
 | 
					 * @param {number} sourceNumber Number to convert to short number
 | 
				
			||||||
 * @returns {ShortNumber} Calculated short number
 | 
					 * @returns {ShortNumber} Calculated short number
 | 
				
			||||||
| 
						 | 
					@ -25,7 +21,8 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
				
			||||||
 * shortNumber(5936);
 | 
					 * shortNumber(5936);
 | 
				
			||||||
 * // => [5.936, 1000, 1]
 | 
					 * // => [5.936, 1000, 1]
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function toShortNumber(sourceNumber) {
 | 
					export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits
 | 
				
			||||||
 | 
					export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			||||||
  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
					  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
				
			||||||
    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
					    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
				
			||||||
  } else if (sourceNumber < DECIMAL_UNITS.MILLION) {
 | 
					  } else if (sourceNumber < DECIMAL_UNITS.MILLION) {
 | 
				
			||||||
| 
						 | 
					@ -59,20 +56,16 @@ export function toShortNumber(sourceNumber) {
 | 
				
			||||||
 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
					 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
				
			||||||
 * // => 1790
 | 
					 * // => 1790
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function pluralReady(sourceNumber, division) {
 | 
					export function pluralReady(sourceNumber: number, division: DecimalUnits): number {
 | 
				
			||||||
  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
 | 
					  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
 | 
				
			||||||
    return sourceNumber;
 | 
					    return sourceNumber;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let closestScale = division / DECIMAL_UNITS.TEN;
 | 
					  const closestScale = division / DECIMAL_UNITS.TEN;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return Math.trunc(sourceNumber / closestScale) * closestScale;
 | 
					  return Math.trunc(sourceNumber / closestScale) * closestScale;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					export function roundTo10(num: number): number {
 | 
				
			||||||
 * @param {number} num
 | 
					 | 
				
			||||||
 * @returns {number}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export function roundTo10(num) {
 | 
					 | 
				
			||||||
  return Math.round(num * 0.1) / 0.1;
 | 
					  return Math.round(num * 0.1) / 0.1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,17 +0,0 @@
 | 
				
			||||||
export const APP_FOCUS   = 'APP_FOCUS';
 | 
					 | 
				
			||||||
export const APP_UNFOCUS = 'APP_UNFOCUS';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const focusApp = () => ({
 | 
					 | 
				
			||||||
  type: APP_FOCUS,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const unfocusApp = () => ({
 | 
					 | 
				
			||||||
  type: APP_UNFOCUS,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const changeLayout = layout => ({
 | 
					 | 
				
			||||||
  type: APP_LAYOUT_CHANGE,
 | 
					 | 
				
			||||||
  layout,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,10 @@
 | 
				
			||||||
 | 
					import { createAction } from '@reduxjs/toolkit';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const focusApp = createAction('APP_FOCUS');
 | 
				
			||||||
 | 
					export const unfocusApp = createAction('APP_UNFOCUS');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChangeLayoutPayload = {
 | 
				
			||||||
 | 
					  layout: 'mobile' | 'single-column' | 'multi-column';
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const changeLayout =
 | 
				
			||||||
 | 
					  createAction<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');
 | 
				
			||||||
| 
						 | 
					@ -84,7 +84,7 @@ const DIGIT_CHARACTERS = [
 | 
				
			||||||
  '~',
 | 
					  '~',
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const decode83 = (str) => {
 | 
					export const decode83 = (str: string) => {
 | 
				
			||||||
  let value = 0;
 | 
					  let value = 0;
 | 
				
			||||||
  let c, digit;
 | 
					  let c, digit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,13 +97,13 @@ export const decode83 = (str) => {
 | 
				
			||||||
  return value;
 | 
					  return value;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const intToRGB = int => ({
 | 
					export const intToRGB = (int: number) => ({
 | 
				
			||||||
  r: Math.max(0, (int >> 16)),
 | 
					  r: Math.max(0, (int >> 16)),
 | 
				
			||||||
  g: Math.max(0, (int >> 8) & 255),
 | 
					  g: Math.max(0, (int >> 8) & 255),
 | 
				
			||||||
  b: Math.max(0, (int & 255)),
 | 
					  b: Math.max(0, (int & 255)),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getAverageFromBlurhash = blurhash => {
 | 
					export const getAverageFromBlurhash = (blurhash: string) => {
 | 
				
			||||||
  if (!blurhash) {
 | 
					  if (!blurhash) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
export default function compareId (id1, id2) {
 | 
					export default function compareId (id1: string, id2: string) {
 | 
				
			||||||
  if (id1 === id2) {
 | 
					  if (id1 === id2) {
 | 
				
			||||||
    return 0;
 | 
					    return 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -1,65 +0,0 @@
 | 
				
			||||||
// @ts-check
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { decode } from 'blurhash';
 | 
					 | 
				
			||||||
import React, { useRef, useEffect } from 'react';
 | 
					 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * @typedef BlurhashPropsBase
 | 
					 | 
				
			||||||
 * @property {string?} hash Hash to render
 | 
					 | 
				
			||||||
 * @property {number} width
 | 
					 | 
				
			||||||
 * Width of the blurred region in pixels. Defaults to 32
 | 
					 | 
				
			||||||
 * @property {number} [height]
 | 
					 | 
				
			||||||
 * Height of the blurred region in pixels. Defaults to width
 | 
					 | 
				
			||||||
 * @property {boolean} [dummy]
 | 
					 | 
				
			||||||
 * Whether dummy mode is enabled. If enabled, nothing is rendered
 | 
					 | 
				
			||||||
 * and canvas left untouched
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Component that is used to render blurred of blurhash string
 | 
					 | 
				
			||||||
 * @param {BlurhashProps} param1 Props of the component
 | 
					 | 
				
			||||||
 * @returns {JSX.Element} Canvas which will render blurred region element to embed
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
function Blurhash({
 | 
					 | 
				
			||||||
  hash,
 | 
					 | 
				
			||||||
  width = 32,
 | 
					 | 
				
			||||||
  height = width,
 | 
					 | 
				
			||||||
  dummy = false,
 | 
					 | 
				
			||||||
  ...canvasProps
 | 
					 | 
				
			||||||
}) {
 | 
					 | 
				
			||||||
  const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    const { current: canvas } = canvasRef;
 | 
					 | 
				
			||||||
    canvas.width = canvas.width; // resets canvas
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (dummy || !hash) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const pixels = decode(hash, width, height);
 | 
					 | 
				
			||||||
      const ctx = canvas.getContext('2d');
 | 
					 | 
				
			||||||
      const imageData = new ImageData(pixels, width, height);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // @ts-expect-error
 | 
					 | 
				
			||||||
      ctx.putImageData(imageData, 0, 0);
 | 
					 | 
				
			||||||
    } catch (err) {
 | 
					 | 
				
			||||||
      console.error('Blurhash decoding failure', { err, hash });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [dummy, hash, width, height]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Blurhash.propTypes = {
 | 
					 | 
				
			||||||
  hash: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
  width: PropTypes.number,
 | 
					 | 
				
			||||||
  height: PropTypes.number,
 | 
					 | 
				
			||||||
  dummy: PropTypes.bool,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default React.memo(Blurhash);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,45 @@
 | 
				
			||||||
 | 
					import { decode } from 'blurhash';
 | 
				
			||||||
 | 
					import React, { useRef, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Props = {
 | 
				
			||||||
 | 
					  hash: string;
 | 
				
			||||||
 | 
					  width?: number;
 | 
				
			||||||
 | 
					  height?: number;
 | 
				
			||||||
 | 
					  dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched
 | 
				
			||||||
 | 
					  children?: never;
 | 
				
			||||||
 | 
					  [key: string]: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function Blurhash({
 | 
				
			||||||
 | 
					  hash,
 | 
				
			||||||
 | 
					  width = 32,
 | 
				
			||||||
 | 
					  height = width,
 | 
				
			||||||
 | 
					  dummy = false,
 | 
				
			||||||
 | 
					  ...canvasProps
 | 
				
			||||||
 | 
					}: Props) {
 | 
				
			||||||
 | 
					  const canvasRef = useRef<HTMLCanvasElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
				
			||||||
 | 
					    const canvas = canvasRef.current!;
 | 
				
			||||||
 | 
					    // eslint-disable-next-line no-self-assign
 | 
				
			||||||
 | 
					    canvas.width = canvas.width; // resets canvas
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (dummy || !hash) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const pixels = decode(hash, width, height);
 | 
				
			||||||
 | 
					      const ctx = canvas.getContext('2d');
 | 
				
			||||||
 | 
					      const imageData = new ImageData(pixels, width, height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ctx?.putImageData(imageData, 0, 0);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      console.error('Blurhash decoding failure', { err, hash });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [dummy, hash, width, height]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default React.memo(Blurhash);
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,9 @@ export default class ColumnBackButton extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (onClick) {
 | 
					    if (onClick) {
 | 
				
			||||||
      onClick();
 | 
					      onClick();
 | 
				
			||||||
    } else if (window.history && window.history.state) {
 | 
					    // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201
 | 
				
			||||||
 | 
					    // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location
 | 
				
			||||||
 | 
					    } else if (router.route.location.key) {
 | 
				
			||||||
      router.history.goBack();
 | 
					      router.history.goBack();
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      router.history.push('/');
 | 
					      router.history.push('/');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,34 +1,36 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import Icon from 'mastodon/components/icon';
 | 
					import { Icon } from './icon';
 | 
				
			||||||
import AnimatedNumber from 'mastodon/components/animated_number';
 | 
					import { AnimatedNumber } from './animated_number';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class IconButton extends React.PureComponent {
 | 
					type Props = {
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
  static propTypes = {
 | 
					  title: string;
 | 
				
			||||||
    className: PropTypes.string,
 | 
					  icon: string;
 | 
				
			||||||
    title: PropTypes.string.isRequired,
 | 
					  onClick?: React.MouseEventHandler<HTMLButtonElement>;
 | 
				
			||||||
    icon: PropTypes.string.isRequired,
 | 
					  onMouseDown?: React.MouseEventHandler<HTMLButtonElement>;
 | 
				
			||||||
    onClick: PropTypes.func,
 | 
					  onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
 | 
				
			||||||
    onMouseDown: PropTypes.func,
 | 
					  onKeyPress?: React.KeyboardEventHandler<HTMLButtonElement>;
 | 
				
			||||||
    onKeyDown: PropTypes.func,
 | 
					  size: number;
 | 
				
			||||||
    onKeyPress: PropTypes.func,
 | 
					  active: boolean;
 | 
				
			||||||
    size: PropTypes.number,
 | 
					  expanded?: boolean;
 | 
				
			||||||
    active: PropTypes.bool,
 | 
					  style?: React.CSSProperties;
 | 
				
			||||||
    expanded: PropTypes.bool,
 | 
					  activeStyle?: React.CSSProperties;
 | 
				
			||||||
    style: PropTypes.object,
 | 
					  disabled: boolean;
 | 
				
			||||||
    activeStyle: PropTypes.object,
 | 
					  inverted?: boolean;
 | 
				
			||||||
    disabled: PropTypes.bool,
 | 
					  animate: boolean;
 | 
				
			||||||
    inverted: PropTypes.bool,
 | 
					  overlay: boolean;
 | 
				
			||||||
    animate: PropTypes.bool,
 | 
					  tabIndex: number;
 | 
				
			||||||
    overlay: PropTypes.bool,
 | 
					  counter?: number;
 | 
				
			||||||
    tabIndex: PropTypes.number,
 | 
					  obfuscateCount?: boolean;
 | 
				
			||||||
    counter: PropTypes.number,
 | 
					  href?: string;
 | 
				
			||||||
    obfuscateCount: PropTypes.bool,
 | 
					  ariaHidden: boolean;
 | 
				
			||||||
    href: PropTypes.string,
 | 
					}
 | 
				
			||||||
    ariaHidden: PropTypes.bool,
 | 
					type States = {
 | 
				
			||||||
  };
 | 
					  activate: boolean,
 | 
				
			||||||
 | 
					  deactivate: boolean,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default class IconButton extends React.PureComponent<Props, States> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static defaultProps = {
 | 
					  static defaultProps = {
 | 
				
			||||||
    size: 18,
 | 
					    size: 18,
 | 
				
			||||||
| 
						 | 
					@ -45,7 +47,7 @@ export default class IconButton extends React.PureComponent {
 | 
				
			||||||
    deactivate: false,
 | 
					    deactivate: false,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillReceiveProps (nextProps) {
 | 
					  UNSAFE_componentWillReceiveProps (nextProps: Props) {
 | 
				
			||||||
    if (!nextProps.animate) return;
 | 
					    if (!nextProps.animate) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.props.active && !nextProps.active) {
 | 
					    if (this.props.active && !nextProps.active) {
 | 
				
			||||||
| 
						 | 
					@ -55,27 +57,27 @@ export default class IconButton extends React.PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleClick = (e) =>  {
 | 
					  handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) =>  {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!this.props.disabled) {
 | 
					    if (!this.props.disabled && this.props.onClick != null) {
 | 
				
			||||||
      this.props.onClick(e);
 | 
					      this.props.onClick(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleKeyPress = (e) => {
 | 
					  handleKeyPress: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
 | 
				
			||||||
    if (this.props.onKeyPress && !this.props.disabled) {
 | 
					    if (this.props.onKeyPress && !this.props.disabled) {
 | 
				
			||||||
      this.props.onKeyPress(e);
 | 
					      this.props.onKeyPress(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleMouseDown = (e) => {
 | 
					  handleMouseDown: React.MouseEventHandler<HTMLButtonElement> = (e) => {
 | 
				
			||||||
    if (!this.props.disabled && this.props.onMouseDown) {
 | 
					    if (!this.props.disabled && this.props.onMouseDown) {
 | 
				
			||||||
      this.props.onMouseDown(e);
 | 
					      this.props.onMouseDown(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleKeyDown = (e) => {
 | 
					  handleKeyDown: React.KeyboardEventHandler<HTMLButtonElement> = (e) => {
 | 
				
			||||||
    if (!this.props.disabled && this.props.onKeyDown) {
 | 
					    if (!this.props.disabled && this.props.onKeyDown) {
 | 
				
			||||||
      this.props.onKeyDown(e);
 | 
					      this.props.onKeyDown(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -132,7 +134,7 @@ export default class IconButton extends React.PureComponent {
 | 
				
			||||||
      </React.Fragment>
 | 
					      </React.Fragment>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (href && !this.prop) {
 | 
					    if (href != null) {
 | 
				
			||||||
      contents = (
 | 
					      contents = (
 | 
				
			||||||
        <a href={href} target='_blank' rel='noopener noreferrer'>
 | 
					        <a href={href} target='_blank' rel='noopener noreferrer'>
 | 
				
			||||||
          {contents}
 | 
					          {contents}
 | 
				
			||||||
| 
						 | 
					@ -81,12 +81,10 @@ class Item extends React.PureComponent {
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
 | 
					    const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let badges = [], thumbnail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let width  = 50;
 | 
					    let width  = 50;
 | 
				
			||||||
    let height = 100;
 | 
					    let height = 100;
 | 
				
			||||||
    let top    = 'auto';
 | 
					 | 
				
			||||||
    let left   = 'auto';
 | 
					 | 
				
			||||||
    let bottom = 'auto';
 | 
					 | 
				
			||||||
    let right  = 'auto';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (size === 1) {
 | 
					    if (size === 1) {
 | 
				
			||||||
      width = 100;
 | 
					      width = 100;
 | 
				
			||||||
| 
						 | 
					@ -96,45 +94,13 @@ class Item extends React.PureComponent {
 | 
				
			||||||
      height = 50;
 | 
					      height = 50;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (size === 2) {
 | 
					    if (attachment.get('description')?.length > 0) {
 | 
				
			||||||
      if (index === 0) {
 | 
					      badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
 | 
				
			||||||
        right = '2px';
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        left = '2px';
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    } else if (size === 3) {
 | 
					 | 
				
			||||||
      if (index === 0) {
 | 
					 | 
				
			||||||
        right = '2px';
 | 
					 | 
				
			||||||
      } else if (index > 0) {
 | 
					 | 
				
			||||||
        left = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (index === 1) {
 | 
					 | 
				
			||||||
        bottom = '2px';
 | 
					 | 
				
			||||||
      } else if (index > 1) {
 | 
					 | 
				
			||||||
        top = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (size === 4) {
 | 
					 | 
				
			||||||
      if (index === 0 || index === 2) {
 | 
					 | 
				
			||||||
        right = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (index === 1 || index === 3) {
 | 
					 | 
				
			||||||
        left = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (index < 2) {
 | 
					 | 
				
			||||||
        bottom = '2px';
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        top = '2px';
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let thumbnail = '';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (attachment.get('type') === 'unknown') {
 | 
					    if (attachment.get('type') === 'unknown') {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
 | 
					        <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
 | 
				
			||||||
          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
 | 
					          <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
 | 
				
			||||||
            <Blurhash
 | 
					            <Blurhash
 | 
				
			||||||
              hash={attachment.get('blurhash')}
 | 
					              hash={attachment.get('blurhash')}
 | 
				
			||||||
| 
						 | 
					@ -184,6 +150,8 @@ class Item extends React.PureComponent {
 | 
				
			||||||
    } else if (attachment.get('type') === 'gifv') {
 | 
					    } else if (attachment.get('type') === 'gifv') {
 | 
				
			||||||
      const autoPlay = this.getAutoPlay();
 | 
					      const autoPlay = this.getAutoPlay();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      badges.push(<span key='gif' className='media-gallery__gifv__label'>GIF</span>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      thumbnail = (
 | 
					      thumbnail = (
 | 
				
			||||||
        <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
 | 
					        <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
 | 
				
			||||||
          <video
 | 
					          <video
 | 
				
			||||||
| 
						 | 
					@ -201,14 +169,12 @@ class Item extends React.PureComponent {
 | 
				
			||||||
            loop
 | 
					            loop
 | 
				
			||||||
            muted
 | 
					            muted
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
 | 
					 | 
				
			||||||
          <span className='media-gallery__gifv__label'>GIF</span>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
 | 
					      <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
 | 
				
			||||||
        <Blurhash
 | 
					        <Blurhash
 | 
				
			||||||
          hash={attachment.get('blurhash')}
 | 
					          hash={attachment.get('blurhash')}
 | 
				
			||||||
          dummy={!useBlurhash}
 | 
					          dummy={!useBlurhash}
 | 
				
			||||||
| 
						 | 
					@ -216,7 +182,14 @@ class Item extends React.PureComponent {
 | 
				
			||||||
            'media-gallery__preview--hidden': visible && this.state.loaded,
 | 
					            'media-gallery__preview--hidden': visible && this.state.loaded,
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {visible && thumbnail}
 | 
					        {visible && thumbnail}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {badges && (
 | 
				
			||||||
 | 
					          <div className='media-gallery__item__badges'>
 | 
				
			||||||
 | 
					            {badges}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -313,7 +286,7 @@ class MediaGallery extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { media, lang, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
 | 
					    const { media, lang, intl, sensitive, defaultWidth, standalone, autoplay } = this.props;
 | 
				
			||||||
    const { visible } = this.state;
 | 
					    const { visible } = this.state;
 | 
				
			||||||
    const width = this.state.width || defaultWidth;
 | 
					    const width = this.state.width || defaultWidth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -322,13 +295,9 @@ class MediaGallery extends React.PureComponent {
 | 
				
			||||||
    const style = {};
 | 
					    const style = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.isFullSizeEligible() && (standalone || !cropImages)) {
 | 
					    if (this.isFullSizeEligible() && (standalone || !cropImages)) {
 | 
				
			||||||
      if (width) {
 | 
					      style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
 | 
				
			||||||
        style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (width) {
 | 
					 | 
				
			||||||
      style.height = width / (16/9);
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      style.height = height;
 | 
					      style.aspectRatio = '16 / 9';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const size     = media.take(4).size;
 | 
					    const size     = media.take(4).size;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,62 +3,22 @@ import PropTypes from 'prop-types';
 | 
				
			||||||
import Icon from 'mastodon/components/icon';
 | 
					import Icon from 'mastodon/components/icon';
 | 
				
			||||||
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
 | 
					import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import { debounce } from 'lodash';
 | 
					 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PictureInPicturePlaceholder extends React.PureComponent {
 | 
					class PictureInPicturePlaceholder extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
    width: PropTypes.number,
 | 
					 | 
				
			||||||
    dispatch: PropTypes.func.isRequired,
 | 
					    dispatch: PropTypes.func.isRequired,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    width: this.props.width,
 | 
					 | 
				
			||||||
    height: this.props.width && (this.props.width / (16/9)),
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleClick = () => {
 | 
					  handleClick = () => {
 | 
				
			||||||
    const { dispatch } = this.props;
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
    dispatch(removePictureInPicture());
 | 
					    dispatch(removePictureInPicture());
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setRef = c => {
 | 
					 | 
				
			||||||
    this.node = c;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.node) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _setDimensions () {
 | 
					 | 
				
			||||||
    const width  = this.node.offsetWidth;
 | 
					 | 
				
			||||||
    const height = width / (16/9);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ width, height });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentDidMount () {
 | 
					 | 
				
			||||||
    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentWillUnmount () {
 | 
					 | 
				
			||||||
    window.removeEventListener('resize', this.handleResize);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleResize = debounce(() => {
 | 
					 | 
				
			||||||
    if (this.node) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, 250, {
 | 
					 | 
				
			||||||
    trailing: true,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { height } = this.state;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex={0} onClick={this.handleClick}>
 | 
					      <div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}>
 | 
				
			||||||
        <Icon id='window-restore' />
 | 
					        <Icon id='window-restore' />
 | 
				
			||||||
        <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
 | 
					        <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -411,7 +411,7 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (pictureInPicture.get('inUse')) {
 | 
					    if (pictureInPicture.get('inUse')) {
 | 
				
			||||||
      media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
 | 
					      media = <PictureInPicturePlaceholder />;
 | 
				
			||||||
    } else if (status.get('media_attachments').size > 0) {
 | 
					    } else if (status.get('media_attachments').size > 0) {
 | 
				
			||||||
      if (this.props.muted) {
 | 
					      if (this.props.muted) {
 | 
				
			||||||
        media = (
 | 
					        media = (
 | 
				
			||||||
| 
						 | 
					@ -460,12 +460,9 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
                src={attachment.get('url')}
 | 
					                src={attachment.get('url')}
 | 
				
			||||||
                alt={attachment.get('description')}
 | 
					                alt={attachment.get('description')}
 | 
				
			||||||
                lang={status.get('language')}
 | 
					                lang={status.get('language')}
 | 
				
			||||||
                width={this.props.cachedMediaWidth}
 | 
					 | 
				
			||||||
                height={110}
 | 
					 | 
				
			||||||
                inline
 | 
					                inline
 | 
				
			||||||
                sensitive={status.get('sensitive')}
 | 
					                sensitive={status.get('sensitive')}
 | 
				
			||||||
                onOpenVideo={this.handleOpenVideo}
 | 
					                onOpenVideo={this.handleOpenVideo}
 | 
				
			||||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
					 | 
				
			||||||
                deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
 | 
					                deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
 | 
				
			||||||
                visible={this.state.showMedia}
 | 
					                visible={this.state.showMedia}
 | 
				
			||||||
                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
					                onToggleVisibility={this.handleToggleMediaVisibility}
 | 
				
			||||||
| 
						 | 
					@ -498,8 +495,6 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
          onOpenMedia={this.handleOpenMedia}
 | 
					          onOpenMedia={this.handleOpenMedia}
 | 
				
			||||||
          card={status.get('card')}
 | 
					          card={status.get('card')}
 | 
				
			||||||
          compact
 | 
					          compact
 | 
				
			||||||
          cacheWidth={this.props.cacheMediaWidth}
 | 
					 | 
				
			||||||
          defaultWidth={this.props.cachedMediaWidth}
 | 
					 | 
				
			||||||
          sensitive={status.get('sensitive')}
 | 
					          sensitive={status.get('sensitive')}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { Provider } from 'react-redux';
 | 
					import { Provider } from 'react-redux';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import configureStore from '../store/configureStore';
 | 
					import { store } from '../store/configureStore';
 | 
				
			||||||
import { hydrateStore } from '../actions/store';
 | 
					import { hydrateStore } from '../actions/store';
 | 
				
			||||||
import { IntlProvider, addLocaleData } from 'react-intl';
 | 
					import { IntlProvider, addLocaleData } from 'react-intl';
 | 
				
			||||||
import { getLocale } from '../locales';
 | 
					import { getLocale } from '../locales';
 | 
				
			||||||
| 
						 | 
					@ -12,8 +12,6 @@ import { fetchCustomEmojis } from '../actions/custom_emojis';
 | 
				
			||||||
const { localeData, messages } = getLocale();
 | 
					const { localeData, messages } = getLocale();
 | 
				
			||||||
addLocaleData(localeData);
 | 
					addLocaleData(localeData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const store = configureStore();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if (initialState) {
 | 
					if (initialState) {
 | 
				
			||||||
  store.dispatch(hydrateStore(initialState));
 | 
					  store.dispatch(hydrateStore(initialState));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl';
 | 
				
			||||||
import { Provider as ReduxProvider } from 'react-redux';
 | 
					import { Provider as ReduxProvider } from 'react-redux';
 | 
				
			||||||
import { BrowserRouter, Route } from 'react-router-dom';
 | 
					import { BrowserRouter, Route } from 'react-router-dom';
 | 
				
			||||||
import { ScrollContext } from 'react-router-scroll-4';
 | 
					import { ScrollContext } from 'react-router-scroll-4';
 | 
				
			||||||
import configureStore from 'mastodon/store/configureStore';
 | 
					import { store } from 'mastodon/store/configureStore';
 | 
				
			||||||
import UI from 'mastodon/features/ui';
 | 
					import UI from 'mastodon/features/ui';
 | 
				
			||||||
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
 | 
					import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
 | 
				
			||||||
import { hydrateStore } from 'mastodon/actions/store';
 | 
					import { hydrateStore } from 'mastodon/actions/store';
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,6 @@ addLocaleData(localeData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
 | 
					const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const store = configureStore();
 | 
					 | 
				
			||||||
const hydrateAction = hydrateStore(initialState);
 | 
					const hydrateAction = hydrateStore(initialState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
store.dispatch(hydrateAction);
 | 
					store.dispatch(hydrateAction);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -384,7 +384,7 @@ class Audio extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _getRadius () {
 | 
					  _getRadius () {
 | 
				
			||||||
    return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2);
 | 
					    return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _getScaleCoefficient () {
 | 
					  _getScaleCoefficient () {
 | 
				
			||||||
| 
						 | 
					@ -396,7 +396,7 @@ class Audio extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _getCY() {
 | 
					  _getCY() {
 | 
				
			||||||
    return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient()));
 | 
					    return Math.floor((this.state.height || this.props.height) / 2);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _getAccentColor () {
 | 
					  _getAccentColor () {
 | 
				
			||||||
| 
						 | 
					@ -470,7 +470,7 @@ class Audio extends React.PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
 | 
					      <div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Blurhash
 | 
					        <Blurhash
 | 
				
			||||||
          hash={blurhash}
 | 
					          hash={blurhash}
 | 
				
			||||||
| 
						 | 
					@ -515,9 +515,16 @@ class Audio extends React.PureComponent {
 | 
				
			||||||
        {(revealed || editable) && <img
 | 
					        {(revealed || editable) && <img
 | 
				
			||||||
          src={this.props.poster}
 | 
					          src={this.props.poster}
 | 
				
			||||||
          alt=''
 | 
					          alt=''
 | 
				
			||||||
          width={(this._getRadius() - TICK_SIZE) * 2}
 | 
					          style={{
 | 
				
			||||||
          height={(this._getRadius() - TICK_SIZE) * 2}
 | 
					            position: 'absolute',
 | 
				
			||||||
          style={{ position: 'absolute', left: this._getCX(), top: this._getCY(), transform: 'translate(-50%, -50%)', borderRadius: '50%', pointerEvents: 'none' }}
 | 
					            left: '50%',
 | 
				
			||||||
 | 
					            top: '50%',
 | 
				
			||||||
 | 
					            height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`,
 | 
				
			||||||
 | 
					            aspectRatio: '1',
 | 
				
			||||||
 | 
					            transform: 'translate(-50%, -50%)',
 | 
				
			||||||
 | 
					            borderRadius: '50%',
 | 
				
			||||||
 | 
					            pointerEvents: 'none',
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
        />}
 | 
					        />}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
 | 
					        <div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,6 @@ import classnames from 'classnames';
 | 
				
			||||||
import Icon from 'mastodon/components/icon';
 | 
					import Icon from 'mastodon/components/icon';
 | 
				
			||||||
import { useBlurhash } from 'mastodon/initial_state';
 | 
					import { useBlurhash } from 'mastodon/initial_state';
 | 
				
			||||||
import Blurhash from 'mastodon/components/blurhash';
 | 
					import Blurhash from 'mastodon/components/blurhash';
 | 
				
			||||||
import { debounce } from 'lodash';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const IDNA_PREFIX = 'xn--';
 | 
					const IDNA_PREFIX = 'xn--';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,8 +53,6 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
    card: ImmutablePropTypes.map,
 | 
					    card: ImmutablePropTypes.map,
 | 
				
			||||||
    onOpenMedia: PropTypes.func.isRequired,
 | 
					    onOpenMedia: PropTypes.func.isRequired,
 | 
				
			||||||
    compact: PropTypes.bool,
 | 
					    compact: PropTypes.bool,
 | 
				
			||||||
    defaultWidth: PropTypes.number,
 | 
					 | 
				
			||||||
    cacheWidth: PropTypes.func,
 | 
					 | 
				
			||||||
    sensitive: PropTypes.bool,
 | 
					    sensitive: PropTypes.bool,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,7 +61,6 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = {
 | 
					  state = {
 | 
				
			||||||
    width: this.props.defaultWidth || 280,
 | 
					 | 
				
			||||||
    previewLoaded: false,
 | 
					    previewLoaded: false,
 | 
				
			||||||
    embedded: false,
 | 
					    embedded: false,
 | 
				
			||||||
    revealed: !this.props.sensitive,
 | 
					    revealed: !this.props.sensitive,
 | 
				
			||||||
| 
						 | 
					@ -87,24 +83,6 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
    window.removeEventListener('resize', this.handleResize);
 | 
					    window.removeEventListener('resize', this.handleResize);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _setDimensions () {
 | 
					 | 
				
			||||||
    const width = this.node.offsetWidth;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.props.cacheWidth) {
 | 
					 | 
				
			||||||
      this.props.cacheWidth(width);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ width });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleResize = debounce(() => {
 | 
					 | 
				
			||||||
    if (this.node) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, 250, {
 | 
					 | 
				
			||||||
    trailing: true,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handlePhotoClick = () => {
 | 
					  handlePhotoClick = () => {
 | 
				
			||||||
    const { card, onOpenMedia } = this.props;
 | 
					    const { card, onOpenMedia } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -138,10 +116,6 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setRef = c => {
 | 
					  setRef = c => {
 | 
				
			||||||
    this.node = c;
 | 
					    this.node = c;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.node) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleImageLoad = () => {
 | 
					  handleImageLoad = () => {
 | 
				
			||||||
| 
						 | 
					@ -157,36 +131,31 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
  renderVideo () {
 | 
					  renderVideo () {
 | 
				
			||||||
    const { card }  = this.props;
 | 
					    const { card }  = this.props;
 | 
				
			||||||
    const content   = { __html: addAutoPlay(card.get('html')) };
 | 
					    const content   = { __html: addAutoPlay(card.get('html')) };
 | 
				
			||||||
    const { width } = this.state;
 | 
					 | 
				
			||||||
    const ratio     = card.get('width') / card.get('height');
 | 
					 | 
				
			||||||
    const height    = width / ratio;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        ref={this.setRef}
 | 
					        ref={this.setRef}
 | 
				
			||||||
        className='status-card__image status-card-video'
 | 
					        className='status-card__image status-card-video'
 | 
				
			||||||
        dangerouslySetInnerHTML={content}
 | 
					        dangerouslySetInnerHTML={content}
 | 
				
			||||||
        style={{ height }}
 | 
					        style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { card, compact } = this.props;
 | 
					    const { card, compact } = this.props;
 | 
				
			||||||
    const { width, embedded, revealed } = this.state;
 | 
					    const { embedded, revealed } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (card === null) {
 | 
					    if (card === null) {
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
 | 
					    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
 | 
				
			||||||
    const horizontal  = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
 | 
					    const horizontal  = (!compact && card.get('width') > card.get('height')) || card.get('type') !== 'link' || embedded;
 | 
				
			||||||
    const interactive = card.get('type') !== 'link';
 | 
					    const interactive = card.get('type') !== 'link';
 | 
				
			||||||
    const className   = classnames('status-card', { horizontal, compact, interactive });
 | 
					    const className   = classnames('status-card', { horizontal, compact, interactive });
 | 
				
			||||||
    const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
 | 
					    const title       = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener noreferrer' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
 | 
				
			||||||
    const language    = card.get('language') || '';
 | 
					    const language    = card.get('language') || '';
 | 
				
			||||||
    const ratio       = card.get('width') / card.get('height');
 | 
					 | 
				
			||||||
    const height      = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const description = (
 | 
					    const description = (
 | 
				
			||||||
      <div className='status-card__content' lang={language}>
 | 
					      <div className='status-card__content' lang={language}>
 | 
				
			||||||
| 
						 | 
					@ -196,6 +165,14 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const thumbnailStyle = {
 | 
				
			||||||
 | 
					      visibility: revealed? null : 'hidden',
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (horizontal) {
 | 
				
			||||||
 | 
					      thumbnailStyle.aspectRatio = (compact && !embedded) ? '16 / 9' : `${card.get('width')} / ${card.get('height')}`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let embed     = '';
 | 
					    let embed     = '';
 | 
				
			||||||
    let canvas = (
 | 
					    let canvas = (
 | 
				
			||||||
      <Blurhash
 | 
					      <Blurhash
 | 
				
			||||||
| 
						 | 
					@ -206,7 +183,7 @@ export default class Card extends React.PureComponent {
 | 
				
			||||||
        dummy={!useBlurhash}
 | 
					        dummy={!useBlurhash}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    let thumbnail = <img src={card.get('image')} alt='' style={{ width: horizontal ? width : null, height: horizontal ? height : null, visibility: revealed ? null : 'hidden' }} onLoad={this.handleImageLoad} className='status-card__image-image' />;
 | 
					    let thumbnail = <img src={card.get('image')} alt='' style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
 | 
				
			||||||
    let spoilerButton = (
 | 
					    let spoilerButton = (
 | 
				
			||||||
      <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
 | 
					      <button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
 | 
				
			||||||
        <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
					        <span className='spoiler-button__overlay__label'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -362,7 +362,7 @@ class UI extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (layout !== this.props.layout) {
 | 
					    if (layout !== this.props.layout) {
 | 
				
			||||||
      this.handleLayoutChange.cancel();
 | 
					      this.handleLayoutChange.cancel();
 | 
				
			||||||
      this.props.dispatch(changeLayout(layout));
 | 
					      this.props.dispatch(changeLayout({ layout }));
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.handleLayoutChange();
 | 
					      this.handleLayoutChange();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import { is } from 'immutable';
 | 
					import { is } from 'immutable';
 | 
				
			||||||
import { throttle, debounce } from 'lodash';
 | 
					import { throttle } from 'lodash';
 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
 | 
					import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen';
 | 
				
			||||||
import { displayMedia, useBlurhash } from '../../initial_state';
 | 
					import { displayMedia, useBlurhash } from '../../initial_state';
 | 
				
			||||||
| 
						 | 
					@ -102,8 +102,6 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    src: PropTypes.string.isRequired,
 | 
					    src: PropTypes.string.isRequired,
 | 
				
			||||||
    alt: PropTypes.string,
 | 
					    alt: PropTypes.string,
 | 
				
			||||||
    lang: PropTypes.string,
 | 
					    lang: PropTypes.string,
 | 
				
			||||||
    width: PropTypes.number,
 | 
					 | 
				
			||||||
    height: PropTypes.number,
 | 
					 | 
				
			||||||
    sensitive: PropTypes.bool,
 | 
					    sensitive: PropTypes.bool,
 | 
				
			||||||
    currentTime: PropTypes.number,
 | 
					    currentTime: PropTypes.number,
 | 
				
			||||||
    onOpenVideo: PropTypes.func,
 | 
					    onOpenVideo: PropTypes.func,
 | 
				
			||||||
| 
						 | 
					@ -112,7 +110,6 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    inline: PropTypes.bool,
 | 
					    inline: PropTypes.bool,
 | 
				
			||||||
    editable: PropTypes.bool,
 | 
					    editable: PropTypes.bool,
 | 
				
			||||||
    alwaysVisible: PropTypes.bool,
 | 
					    alwaysVisible: PropTypes.bool,
 | 
				
			||||||
    cacheWidth: PropTypes.func,
 | 
					 | 
				
			||||||
    visible: PropTypes.bool,
 | 
					    visible: PropTypes.bool,
 | 
				
			||||||
    onToggleVisibility: PropTypes.func,
 | 
					    onToggleVisibility: PropTypes.func,
 | 
				
			||||||
    deployPictureInPicture: PropTypes.func,
 | 
					    deployPictureInPicture: PropTypes.func,
 | 
				
			||||||
| 
						 | 
					@ -135,7 +132,6 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    volume: 0.5,
 | 
					    volume: 0.5,
 | 
				
			||||||
    paused: true,
 | 
					    paused: true,
 | 
				
			||||||
    dragging: false,
 | 
					    dragging: false,
 | 
				
			||||||
    containerWidth: this.props.width,
 | 
					 | 
				
			||||||
    fullscreen: false,
 | 
					    fullscreen: false,
 | 
				
			||||||
    hovered: false,
 | 
					    hovered: false,
 | 
				
			||||||
    muted: false,
 | 
					    muted: false,
 | 
				
			||||||
| 
						 | 
					@ -144,24 +140,8 @@ class Video extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setPlayerRef = c => {
 | 
					  setPlayerRef = c => {
 | 
				
			||||||
    this.player = c;
 | 
					    this.player = c;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.player) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _setDimensions () {
 | 
					 | 
				
			||||||
    const width = this.player.offsetWidth;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.props.cacheWidth) {
 | 
					 | 
				
			||||||
      this.props.cacheWidth(width);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({
 | 
					 | 
				
			||||||
      containerWidth: width,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setVideoRef = c => {
 | 
					  setVideoRef = c => {
 | 
				
			||||||
    this.video = c;
 | 
					    this.video = c;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -370,12 +350,10 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
 | 
					    document.addEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    window.addEventListener('scroll', this.handleScroll);
 | 
					    window.addEventListener('scroll', this.handleScroll);
 | 
				
			||||||
    window.addEventListener('resize', this.handleResize, { passive: true });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillUnmount () {
 | 
					  componentWillUnmount () {
 | 
				
			||||||
    window.removeEventListener('scroll', this.handleScroll);
 | 
					    window.removeEventListener('scroll', this.handleScroll);
 | 
				
			||||||
    window.removeEventListener('resize', this.handleResize);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
 | 
					    document.removeEventListener('fullscreenchange', this.handleFullscreenChange, true);
 | 
				
			||||||
    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
 | 
					    document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
 | 
				
			||||||
| 
						 | 
					@ -404,14 +382,6 @@ class Video extends React.PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleResize = debounce(() => {
 | 
					 | 
				
			||||||
    if (this.player) {
 | 
					 | 
				
			||||||
      this._setDimensions();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, 250, {
 | 
					 | 
				
			||||||
    trailing: true,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleScroll = throttle(() => {
 | 
					  handleScroll = throttle(() => {
 | 
				
			||||||
    if (!this.video) {
 | 
					    if (!this.video) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
| 
						 | 
					@ -525,17 +495,12 @@ class Video extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
 | 
					    const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
 | 
				
			||||||
    const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
 | 
					    const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
 | 
				
			||||||
    const progress = Math.min((currentTime / duration) * 100, 100);
 | 
					    const progress = Math.min((currentTime / duration) * 100, 100);
 | 
				
			||||||
    const playerStyle = {};
 | 
					    const playerStyle = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let { width, height } = this.props;
 | 
					    if (inline) {
 | 
				
			||||||
 | 
					      playerStyle.aspectRatio = '16 / 9';
 | 
				
			||||||
    if (inline && containerWidth) {
 | 
					 | 
				
			||||||
      width  = containerWidth;
 | 
					 | 
				
			||||||
      height = containerWidth / (16/9);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      playerStyle.height = height;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let preload;
 | 
					    let preload;
 | 
				
			||||||
| 
						 | 
					@ -586,8 +551,6 @@ class Video extends React.PureComponent {
 | 
				
			||||||
          aria-label={alt}
 | 
					          aria-label={alt}
 | 
				
			||||||
          title={alt}
 | 
					          title={alt}
 | 
				
			||||||
          lang={lang}
 | 
					          lang={lang}
 | 
				
			||||||
          width={width}
 | 
					 | 
				
			||||||
          height={height}
 | 
					 | 
				
			||||||
          volume={volume}
 | 
					          volume={volume}
 | 
				
			||||||
          onClick={this.togglePlay}
 | 
					          onClick={this.togglePlay}
 | 
				
			||||||
          onKeyDown={this.handleVideoKeyDown}
 | 
					          onKeyDown={this.handleVideoKeyDown}
 | 
				
			||||||
| 
						 | 
					@ -596,6 +559,7 @@ class Video extends React.PureComponent {
 | 
				
			||||||
          onLoadedData={this.handleLoadedData}
 | 
					          onLoadedData={this.handleLoadedData}
 | 
				
			||||||
          onProgress={this.handleProgress}
 | 
					          onProgress={this.handleProgress}
 | 
				
			||||||
          onVolumeChange={this.handleVolumeChange}
 | 
					          onVolumeChange={this.handleVolumeChange}
 | 
				
			||||||
 | 
					          style={{ ...playerStyle, width: '100%' }}
 | 
				
			||||||
        />}
 | 
					        />}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
 | 
					        <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,21 +1,12 @@
 | 
				
			||||||
// @ts-check
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { supportsPassiveEvents } from 'detect-passive-events';
 | 
					import { supportsPassiveEvents } from 'detect-passive-events';
 | 
				
			||||||
// @ts-expect-error
 | 
					import { forceSingleColumn } from './initial_state';
 | 
				
			||||||
import { forceSingleColumn } from 'mastodon/initial_state';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const LAYOUT_BREAKPOINT = 630;
 | 
					const LAYOUT_BREAKPOINT = 630;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
 | 
				
			||||||
 * @param {number} width
 | 
					 | 
				
			||||||
 * @returns {boolean}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export const isMobile = width => width <= LAYOUT_BREAKPOINT;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
 | 
				
			||||||
 * @returns {string}
 | 
					export const layoutFromWindow = (): LayoutType => {
 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export const layoutFromWindow = () => {
 | 
					 | 
				
			||||||
  if (isMobile(window.innerWidth)) {
 | 
					  if (isMobile(window.innerWidth)) {
 | 
				
			||||||
    return 'mobile';
 | 
					    return 'mobile';
 | 
				
			||||||
  } else if (forceSingleColumn) {
 | 
					  } else if (forceSingleColumn) {
 | 
				
			||||||
| 
						 | 
					@ -25,8 +16,9 @@ export const layoutFromWindow = () => {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 | 
				
			||||||
// @ts-expect-error
 | 
					// @ts-expect-error
 | 
				
			||||||
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
 | 
					const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && window.MSStream != null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 | 
					const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,8 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import ReactDOM from 'react-dom';
 | 
					import ReactDOM from 'react-dom';
 | 
				
			||||||
import { setupBrowserNotifications } from 'mastodon/actions/notifications';
 | 
					import { setupBrowserNotifications } from 'mastodon/actions/notifications';
 | 
				
			||||||
import Mastodon, { store } from 'mastodon/containers/mastodon';
 | 
					import Mastodon from 'mastodon/containers/mastodon';
 | 
				
			||||||
 | 
					import { store } from 'mastodon/store/configureStore';
 | 
				
			||||||
import { me } from 'mastodon/initial_state';
 | 
					import { me } from 'mastodon/initial_state';
 | 
				
			||||||
import ready from 'mastodon/ready';
 | 
					import ready from 'mastodon/ready';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { STORE_HYDRATE } from 'mastodon/actions/store';
 | 
					import { STORE_HYDRATE } from 'mastodon/actions/store';
 | 
				
			||||||
import { APP_LAYOUT_CHANGE } from 'mastodon/actions/app';
 | 
					import { changeLayout } from 'mastodon/actions/app';
 | 
				
			||||||
import { Map as ImmutableMap } from 'immutable';
 | 
					import { Map as ImmutableMap } from 'immutable';
 | 
				
			||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
 | 
					import { layoutFromWindow } from 'mastodon/is_mobile';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,8 +14,8 @@ export default function meta(state = initialState, action) {
 | 
				
			||||||
  switch(action.type) {
 | 
					  switch(action.type) {
 | 
				
			||||||
  case STORE_HYDRATE:
 | 
					  case STORE_HYDRATE:
 | 
				
			||||||
    return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions']));
 | 
					    return state.merge(action.state.get('meta')).set('permissions', action.state.getIn(['role', 'permissions']));
 | 
				
			||||||
  case APP_LAYOUT_CHANGE:
 | 
					  case changeLayout.type:
 | 
				
			||||||
    return state.set('layout', action.layout);
 | 
					    return state.set('layout', action.payload.layout);
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,21 +0,0 @@
 | 
				
			||||||
import { Map as ImmutableMap } from 'immutable';
 | 
					 | 
				
			||||||
import { NOTIFICATIONS_UPDATE } from 'mastodon/actions/notifications';
 | 
					 | 
				
			||||||
import { APP_FOCUS, APP_UNFOCUS } from 'mastodon/actions/app';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const initialState = ImmutableMap({
 | 
					 | 
				
			||||||
  focused: true,
 | 
					 | 
				
			||||||
  unread: 0,
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default function missed_updates(state = initialState, action) {
 | 
					 | 
				
			||||||
  switch(action.type) {
 | 
					 | 
				
			||||||
  case APP_FOCUS:
 | 
					 | 
				
			||||||
    return state.set('focused', true).set('unread', 0);
 | 
					 | 
				
			||||||
  case APP_UNFOCUS:
 | 
					 | 
				
			||||||
    return state.set('focused', false);
 | 
					 | 
				
			||||||
  case NOTIFICATIONS_UPDATE:
 | 
					 | 
				
			||||||
    return state.get('focused') ? state : state.update('unread', x => x + 1);
 | 
					 | 
				
			||||||
  default:
 | 
					 | 
				
			||||||
    return state;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					import { Record } from 'immutable';
 | 
				
			||||||
 | 
					import type { Action } from 'redux';
 | 
				
			||||||
 | 
					import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
 | 
				
			||||||
 | 
					import { focusApp, unfocusApp } from '../actions/app';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type MissedUpdatesState = {
 | 
				
			||||||
 | 
					  focused: boolean;
 | 
				
			||||||
 | 
					  unread: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					const initialState = Record<MissedUpdatesState>({
 | 
				
			||||||
 | 
					  focused: true,
 | 
				
			||||||
 | 
					  unread: 0,
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function missed_updates(
 | 
				
			||||||
 | 
					  state = initialState,
 | 
				
			||||||
 | 
					  action: Action<string>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  switch (action.type) {
 | 
				
			||||||
 | 
					  case focusApp.type:
 | 
				
			||||||
 | 
					    return state.set('focused', true).set('unread', 0);
 | 
				
			||||||
 | 
					  case unfocusApp.type:
 | 
				
			||||||
 | 
					    return state.set('focused', false);
 | 
				
			||||||
 | 
					  case NOTIFICATIONS_UPDATE:
 | 
				
			||||||
 | 
					    return state.get('focused')
 | 
				
			||||||
 | 
					      ? state
 | 
				
			||||||
 | 
					      : state.update('unread', (x) => x + 1);
 | 
				
			||||||
 | 
					  default:
 | 
				
			||||||
 | 
					    return state;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -23,8 +23,8 @@ import {
 | 
				
			||||||
  MARKERS_FETCH_SUCCESS,
 | 
					  MARKERS_FETCH_SUCCESS,
 | 
				
			||||||
} from '../actions/markers';
 | 
					} from '../actions/markers';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  APP_FOCUS,
 | 
					  focusApp,
 | 
				
			||||||
  APP_UNFOCUS,
 | 
					  unfocusApp,
 | 
				
			||||||
} from '../actions/app';
 | 
					} from '../actions/app';
 | 
				
			||||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
 | 
					import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
 | 
				
			||||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
 | 
					import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
 | 
				
			||||||
| 
						 | 
					@ -258,9 +258,9 @@ export default function notifications(state = initialState, action) {
 | 
				
			||||||
    return updateMounted(state);
 | 
					    return updateMounted(state);
 | 
				
			||||||
  case NOTIFICATIONS_UNMOUNT:
 | 
					  case NOTIFICATIONS_UNMOUNT:
 | 
				
			||||||
    return state.update('mounted', count => count - 1);
 | 
					    return state.update('mounted', count => count - 1);
 | 
				
			||||||
  case APP_FOCUS:
 | 
					  case focusApp.type:
 | 
				
			||||||
    return updateVisibility(state, true);
 | 
					    return updateVisibility(state, true);
 | 
				
			||||||
  case APP_UNFOCUS:
 | 
					  case unfocusApp.type:
 | 
				
			||||||
    return updateVisibility(state, false);
 | 
					    return updateVisibility(state, false);
 | 
				
			||||||
  case NOTIFICATIONS_LOAD_PENDING:
 | 
					  case NOTIFICATIONS_LOAD_PENDING:
 | 
				
			||||||
    return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
 | 
					    return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
 | 
					const easingOutQuint = (x: number, t: number, b: number, c: number, d: number) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
 | 
				
			||||||
 | 
					const scroll = (node: Element, key: 'scrollTop' | 'scrollLeft', target: number) => {
 | 
				
			||||||
const scroll = (node, key, target) => {
 | 
					 | 
				
			||||||
  const startTime = Date.now();
 | 
					  const startTime = Date.now();
 | 
				
			||||||
  const offset    = node[key];
 | 
					  const offset    = node[key];
 | 
				
			||||||
  const gap       = target - offset;
 | 
					  const gap       = target - offset;
 | 
				
			||||||
| 
						 | 
					@ -28,5 +27,5 @@ const scroll = (node, key, target) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;
 | 
					const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
 | 
					export const scrollRight = (node: Element, position: number) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position);
 | 
				
			||||||
export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
 | 
					export const scrollTop = (node: Element) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0);
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,16 @@
 | 
				
			||||||
import { createStore, applyMiddleware, compose } from 'redux';
 | 
					import { configureStore } from '@reduxjs/toolkit';
 | 
				
			||||||
import thunk from 'redux-thunk';
 | 
					import thunk from 'redux-thunk';
 | 
				
			||||||
import appReducer from '../reducers';
 | 
					import appReducer from '../reducers';
 | 
				
			||||||
import loadingBarMiddleware from '../middleware/loading_bar';
 | 
					import loadingBarMiddleware from '../middleware/loading_bar';
 | 
				
			||||||
import errorsMiddleware from '../middleware/errors';
 | 
					import errorsMiddleware from '../middleware/errors';
 | 
				
			||||||
import soundsMiddleware from '../middleware/sounds';
 | 
					import soundsMiddleware from '../middleware/sounds';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function configureStore() {
 | 
					export const store = configureStore({
 | 
				
			||||||
  return createStore(appReducer, compose(applyMiddleware(
 | 
					  reducer: appReducer,
 | 
				
			||||||
 | 
					  middleware: [
 | 
				
			||||||
    thunk,
 | 
					    thunk,
 | 
				
			||||||
    loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
 | 
					    loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }),
 | 
				
			||||||
    errorsMiddleware(),
 | 
					    errorsMiddleware(),
 | 
				
			||||||
    soundsMiddleware(),
 | 
					    soundsMiddleware(),
 | 
				
			||||||
  ), window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() : f => f));
 | 
					  ],
 | 
				
			||||||
}
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
export const decode = base64 => {
 | 
					export const decode = (base64: string): Uint8Array => {
 | 
				
			||||||
  const rawData = window.atob(base64);
 | 
					  const rawData = window.atob(base64);
 | 
				
			||||||
  const outputArray = new Uint8Array(rawData.length);
 | 
					  const outputArray = new Uint8Array(rawData.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
export const toServerSideType = columnType => {
 | 
					export const toServerSideType = (columnType: string) => {
 | 
				
			||||||
  switch (columnType) {
 | 
					  switch (columnType) {
 | 
				
			||||||
  case 'home':
 | 
					  case 'home':
 | 
				
			||||||
  case 'notifications':
 | 
					  case 'notifications':
 | 
				
			||||||
| 
						 | 
					@ -1,23 +1,19 @@
 | 
				
			||||||
// @ts-check
 | 
					import type { ValueOf } from '../../types/util';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DECIMAL_UNITS = Object.freeze({
 | 
					export const DECIMAL_UNITS = Object.freeze({
 | 
				
			||||||
  ONE: 1,
 | 
					  ONE: 1,
 | 
				
			||||||
  TEN: 10,
 | 
					  TEN: 10,
 | 
				
			||||||
  HUNDRED: Math.pow(10, 2),
 | 
					  HUNDRED: 100,
 | 
				
			||||||
  THOUSAND: Math.pow(10, 3),
 | 
					  THOUSAND: 1_000,
 | 
				
			||||||
  MILLION: Math.pow(10, 6),
 | 
					  MILLION: 1_000_000,
 | 
				
			||||||
  BILLION: Math.pow(10, 9),
 | 
					  BILLION: 1_000_000_000,
 | 
				
			||||||
  TRILLION: Math.pow(10, 12),
 | 
					  TRILLION: 1_000_000_000_000,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					export type DecimalUnits = ValueOf<typeof DECIMAL_UNITS>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
 | 
					const TEN_THOUSAND = DECIMAL_UNITS.THOUSAND * 10;
 | 
				
			||||||
const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
					const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * @typedef {[number, number, number]} ShortNumber
 | 
					 | 
				
			||||||
 * Array of: shorten number, unit of shorten number and maximum fraction digits
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * @param {number} sourceNumber Number to convert to short number
 | 
					 * @param {number} sourceNumber Number to convert to short number
 | 
				
			||||||
 * @returns {ShortNumber} Calculated short number
 | 
					 * @returns {ShortNumber} Calculated short number
 | 
				
			||||||
| 
						 | 
					@ -25,7 +21,8 @@ const TEN_MILLIONS = DECIMAL_UNITS.MILLION * 10;
 | 
				
			||||||
 * shortNumber(5936);
 | 
					 * shortNumber(5936);
 | 
				
			||||||
 * // => [5.936, 1000, 1]
 | 
					 * // => [5.936, 1000, 1]
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function toShortNumber(sourceNumber) {
 | 
					export type ShortNumber = [number, DecimalUnits, 0 | 1] // Array of: shorten number, unit of shorten number and maximum fraction digits
 | 
				
			||||||
 | 
					export function toShortNumber(sourceNumber: number): ShortNumber {
 | 
				
			||||||
  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
					  if (sourceNumber < DECIMAL_UNITS.THOUSAND) {
 | 
				
			||||||
    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
					    return [sourceNumber, DECIMAL_UNITS.ONE, 0];
 | 
				
			||||||
  } else if (sourceNumber < DECIMAL_UNITS.MILLION) {
 | 
					  } else if (sourceNumber < DECIMAL_UNITS.MILLION) {
 | 
				
			||||||
| 
						 | 
					@ -59,20 +56,16 @@ export function toShortNumber(sourceNumber) {
 | 
				
			||||||
 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
					 * pluralReady(1793, DECIMAL_UNITS.THOUSAND)
 | 
				
			||||||
 * // => 1790
 | 
					 * // => 1790
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function pluralReady(sourceNumber, division) {
 | 
					export function pluralReady(sourceNumber: number, division: DecimalUnits): number {
 | 
				
			||||||
  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
 | 
					  if (division == null || division < DECIMAL_UNITS.HUNDRED) {
 | 
				
			||||||
    return sourceNumber;
 | 
					    return sourceNumber;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let closestScale = division / DECIMAL_UNITS.TEN;
 | 
					  const closestScale = division / DECIMAL_UNITS.TEN;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return Math.trunc(sourceNumber / closestScale) * closestScale;
 | 
					  return Math.trunc(sourceNumber / closestScale) * closestScale;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					export function roundTo10(num: number): number {
 | 
				
			||||||
 * @param {number} num
 | 
					 | 
				
			||||||
 * @returns {number}
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export function roundTo10(num) {
 | 
					 | 
				
			||||||
  return Math.round(num * 0.1) / 0.1;
 | 
					  return Math.round(num * 0.1) / 0.1;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1784,7 +1784,6 @@ a.account__display-name {
 | 
				
			||||||
.status__avatar {
 | 
					.status__avatar {
 | 
				
			||||||
  width: 46px;
 | 
					  width: 46px;
 | 
				
			||||||
  height: 46px;
 | 
					  height: 46px;
 | 
				
			||||||
  box-shadow: 0 0 0 2px $ui-base-color;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.muted {
 | 
					.muted {
 | 
				
			||||||
| 
						 | 
					@ -3110,6 +3109,10 @@ $ui-header-height: 55px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.compose-form__highlightable {
 | 
					.compose-form__highlightable {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  flex: 0 1 auto;
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  transition: box-shadow 300ms linear;
 | 
					  transition: box-shadow 300ms linear;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3804,6 +3807,10 @@ a.status-card {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.status-card-video {
 | 
					.status-card-video {
 | 
				
			||||||
 | 
					  // Firefox has a bug where frameborder=0 iframes add some extra blank space
 | 
				
			||||||
 | 
					  // see https://bugzilla.mozilla.org/show_bug.cgi?id=155174
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  iframe {
 | 
					  iframe {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
| 
						 | 
					@ -6326,30 +6333,25 @@ a.status-card.compact:hover {
 | 
				
			||||||
  z-index: 9999;
 | 
					  z-index: 9999;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-gallery__gifv__label {
 | 
					.media-gallery__item__badges {
 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  color: $primary-text-color;
 | 
					 | 
				
			||||||
  background: rgba($base-overlay-background, 0.5);
 | 
					 | 
				
			||||||
  bottom: 6px;
 | 
					  bottom: 6px;
 | 
				
			||||||
  inset-inline-start: 6px;
 | 
					  inset-inline-start: 6px;
 | 
				
			||||||
  padding: 2px 6px;
 | 
					  display: flex;
 | 
				
			||||||
  border-radius: 2px;
 | 
					  gap: 2px;
 | 
				
			||||||
  font-size: 11px;
 | 
					 | 
				
			||||||
  font-weight: 600;
 | 
					 | 
				
			||||||
  z-index: 1;
 | 
					 | 
				
			||||||
  pointer-events: none;
 | 
					 | 
				
			||||||
  opacity: 0.9;
 | 
					 | 
				
			||||||
  transition: opacity 0.1s ease;
 | 
					 | 
				
			||||||
  line-height: 18px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-gallery__gifv {
 | 
					.media-gallery__gifv__label {
 | 
				
			||||||
  &:hover {
 | 
					  display: block;
 | 
				
			||||||
    .media-gallery__gifv__label {
 | 
					  color: $white;
 | 
				
			||||||
      opacity: 1;
 | 
					  background: rgba($black, 0.65);
 | 
				
			||||||
    }
 | 
					  padding: 2px 6px;
 | 
				
			||||||
  }
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  font-size: 11px;
 | 
				
			||||||
 | 
					  font-weight: 700;
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					  line-height: 18px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.attachment-list {
 | 
					.attachment-list {
 | 
				
			||||||
| 
						 | 
					@ -6424,17 +6426,28 @@ a.status-card.compact:hover {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
  min-height: 64px;
 | 
					  min-height: 64px;
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: 50% 50%;
 | 
				
			||||||
 | 
					  grid-template-rows: 50% 50%;
 | 
				
			||||||
 | 
					  gap: 2px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-gallery__item {
 | 
					.media-gallery__item {
 | 
				
			||||||
  border: 0;
 | 
					  border: 0;
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: border-box;
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  float: left;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  border-radius: 4px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &--tall {
 | 
				
			||||||
 | 
					    grid-row: span 2;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &--wide {
 | 
				
			||||||
 | 
					    grid-column: span 2;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.standalone {
 | 
					  &.standalone {
 | 
				
			||||||
    .media-gallery__item-gifv-thumbnail {
 | 
					    .media-gallery__item-gifv-thumbnail {
 | 
				
			||||||
      transform: none;
 | 
					      transform: none;
 | 
				
			||||||
| 
						 | 
					@ -8332,6 +8345,7 @@ noscript {
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  color: $darker-text-color;
 | 
					  color: $darker-text-color;
 | 
				
			||||||
 | 
					  aspect-ratio: 16 / 9;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  i {
 | 
					  i {
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,4 @@
 | 
				
			||||||
interface MastodonMap<T> {
 | 
					import type { Record } from 'immutable';
 | 
				
			||||||
  get<K extends keyof T>(key: K): T[K];
 | 
					 | 
				
			||||||
  has<K extends keyof T>(key: K): boolean;
 | 
					 | 
				
			||||||
  set<K extends keyof T>(key: K, value: T[K]): this;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
type AccountValues = {
 | 
					type AccountValues = {
 | 
				
			||||||
  id: number;
 | 
					  id: number;
 | 
				
			||||||
| 
						 | 
					@ -10,4 +6,5 @@ type AccountValues = {
 | 
				
			||||||
  avatar_static: string;
 | 
					  avatar_static: string;
 | 
				
			||||||
  [key: string]: any;
 | 
					  [key: string]: any;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
export type Account = MastodonMap<AccountValues>;
 | 
					
 | 
				
			||||||
 | 
					export type Account = Record<AccountValues>;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					export type ValueOf<T> = T[keyof T];
 | 
				
			||||||
| 
						 | 
					@ -43,7 +43,7 @@ class ActivityTracker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    case @type
 | 
					    case @type
 | 
				
			||||||
    when :basic
 | 
					    when :basic
 | 
				
			||||||
      redis.mget(*keys).map(&:to_i).sum
 | 
					      redis.mget(*keys).sum(&:to_i)
 | 
				
			||||||
    when :unique
 | 
					    when :unique
 | 
				
			||||||
      redis.pfcount(*keys)
 | 
					      redis.pfcount(*keys)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
 | 
				
			||||||
  def perform
 | 
					  def perform
 | 
				
			||||||
    return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
 | 
					    return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with_lock("announce:#{value_or_id(@object)}") do
 | 
					    with_redis_lock("announce:#{value_or_id(@object)}") do
 | 
				
			||||||
      original_status = status_from_object
 | 
					      original_status = status_from_object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return reject_payload! if original_status.nil? || !announceable?(original_status)
 | 
					      return reject_payload! if original_status.nil? || !announceable?(original_status)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 | 
				
			||||||
  def create_status
 | 
					  def create_status
 | 
				
			||||||
    return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity?
 | 
					    return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with_lock("create:#{object_uri}") do
 | 
					    with_redis_lock("create:#{object_uri}") do
 | 
				
			||||||
      return if delete_arrived_first?(object_uri) || poll_vote?
 | 
					      return if delete_arrived_first?(object_uri) || poll_vote?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @status = find_existing_status
 | 
					      @status = find_existing_status
 | 
				
			||||||
| 
						 | 
					@ -313,7 +313,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 | 
				
			||||||
    poll = replied_to_status.preloadable_poll
 | 
					    poll = replied_to_status.preloadable_poll
 | 
				
			||||||
    already_voted = true
 | 
					    already_voted = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do
 | 
					    with_redis_lock("vote:#{replied_to_status.poll_id}:#{@account.id}") do
 | 
				
			||||||
      already_voted = poll.votes.where(account: @account).exists?
 | 
					      already_voted = poll.votes.where(account: @account).exists?
 | 
				
			||||||
      poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
 | 
					      poll.votes.create!(account: @account, choice: poll.options.index(@object['name']), uri: object_uri)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def delete_person
 | 
					  def delete_person
 | 
				
			||||||
    with_lock("delete_in_progress:#{@account.id}", autorelease: 2.hours, raise_on_failure: false) do
 | 
					    with_redis_lock("delete_in_progress:#{@account.id}", autorelease: 2.hours, raise_on_failure: false) do
 | 
				
			||||||
      DeleteAccountService.new.call(@account, reserve_username: false, skip_activitypub: true)
 | 
					      DeleteAccountService.new.call(@account, reserve_username: false, skip_activitypub: true)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -20,14 +20,14 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
 | 
				
			||||||
  def delete_note
 | 
					  def delete_note
 | 
				
			||||||
    return if object_uri.nil?
 | 
					    return if object_uri.nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do
 | 
					    with_redis_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do
 | 
				
			||||||
      unless non_matching_uri_hosts?(@account.uri, object_uri)
 | 
					      unless non_matching_uri_hosts?(@account.uri, object_uri)
 | 
				
			||||||
        # This lock ensures a concurrent `ActivityPub::Activity::Create` either
 | 
					        # This lock ensures a concurrent `ActivityPub::Activity::Create` either
 | 
				
			||||||
        # does not create a status at all, or has finished saving it to the
 | 
					        # does not create a status at all, or has finished saving it to the
 | 
				
			||||||
        # database before we try to load it.
 | 
					        # database before we try to load it.
 | 
				
			||||||
        # Without the lock, `delete_later!` could be called after `delete_arrived_first?`
 | 
					        # Without the lock, `delete_later!` could be called after `delete_arrived_first?`
 | 
				
			||||||
        # and `Status.find` before `Status.create!`
 | 
					        # and `Status.find` before `Status.create!`
 | 
				
			||||||
        with_lock("create:#{object_uri}") { delete_later!(object_uri) }
 | 
					        with_redis_lock("create:#{object_uri}") { delete_later!(object_uri) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Tombstone.find_or_create_by(uri: object_uri, account: @account)
 | 
					        Tombstone.find_or_create_by(uri: object_uri, account: @account)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ module ActivityPub::CaseTransform
 | 
				
			||||||
      when Symbol then camel_lower(value.to_s).to_sym
 | 
					      when Symbol then camel_lower(value.to_s).to_sym
 | 
				
			||||||
      when String
 | 
					      when String
 | 
				
			||||||
        camel_lower_cache[value] ||= if value.start_with?('_:')
 | 
					        camel_lower_cache[value] ||= if value.start_with?('_:')
 | 
				
			||||||
                                       "_:#{value.gsub(/\A_:/, '').underscore.camelize(:lower)}"
 | 
					                                       "_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
 | 
				
			||||||
                                     else
 | 
					                                     else
 | 
				
			||||||
                                       value.underscore.camelize(:lower)
 | 
					                                       value.underscore.camelize(:lower)
 | 
				
			||||||
                                     end
 | 
					                                     end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -407,10 +407,10 @@ class FeedManager
 | 
				
			||||||
    return true  if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
 | 
					    return true  if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    check_for_blocks = crutches[:active_mentions][status.id] || []
 | 
					    check_for_blocks = crutches[:active_mentions][status.id] || []
 | 
				
			||||||
    check_for_blocks.concat([status.account_id])
 | 
					    check_for_blocks.push(status.account_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if status.reblog?
 | 
					    if status.reblog?
 | 
				
			||||||
      check_for_blocks.concat([status.reblog.account_id])
 | 
					      check_for_blocks.push(status.reblog.account_id)
 | 
				
			||||||
      check_for_blocks.concat(crutches[:active_mentions][status.reblog_of_id] || [])
 | 
					      check_for_blocks.concat(crutches[:active_mentions][status.reblog_of_id] || [])
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -446,7 +446,7 @@ class FeedManager
 | 
				
			||||||
    # the notification has been checked for mute/block. Therefore, it's not
 | 
					    # the notification has been checked for mute/block. Therefore, it's not
 | 
				
			||||||
    # necessary to check the author of the toot for mute/block again
 | 
					    # necessary to check the author of the toot for mute/block again
 | 
				
			||||||
    check_for_blocks = status.active_mentions.pluck(:account_id)
 | 
					    check_for_blocks = status.active_mentions.pluck(:account_id)
 | 
				
			||||||
    check_for_blocks.concat([status.in_reply_to_account]) if status.reply? && !status.in_reply_to_account_id.nil?
 | 
					    check_for_blocks.push(status.in_reply_to_account) if status.reply? && !status.in_reply_to_account_id.nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    should_filter   = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions)                                                         # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted)
 | 
					    should_filter   = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions)                                                         # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted)
 | 
				
			||||||
    should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
 | 
					    should_filter ||= (status.account.silenced? && !Follow.where(account_id: receiver_id, target_account_id: status.account_id).exists?) # of if the account is silenced and I'm not following them
 | 
				
			||||||
| 
						 | 
					@ -593,10 +593,10 @@ class FeedManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    check_for_blocks = statuses.flat_map do |s|
 | 
					    check_for_blocks = statuses.flat_map do |s|
 | 
				
			||||||
      arr = crutches[:active_mentions][s.id] || []
 | 
					      arr = crutches[:active_mentions][s.id] || []
 | 
				
			||||||
      arr.concat([s.account_id])
 | 
					      arr.push(s.account_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if s.reblog?
 | 
					      if s.reblog?
 | 
				
			||||||
        arr.concat([s.reblog.account_id])
 | 
					        arr.push(s.reblog.account_id)
 | 
				
			||||||
        arr.concat(crutches[:active_mentions][s.reblog_of_id] || [])
 | 
					        arr.concat(crutches[:active_mentions][s.reblog_of_id] || [])
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,8 +6,8 @@ class Importer::AccountsIndexImporter < Importer::BaseImporter
 | 
				
			||||||
      in_work_unit(tmp) do |accounts|
 | 
					      in_work_unit(tmp) do |accounts|
 | 
				
			||||||
        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: accounts).bulk_body
 | 
					        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: accounts).bulk_body
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        indexed = bulk.select { |entry| entry[:index] }.size
 | 
					        indexed = bulk.count { |entry| entry[:index] }
 | 
				
			||||||
        deleted = bulk.select { |entry| entry[:delete] }.size
 | 
					        deleted = bulk.count { |entry| entry[:delete] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 | 
					        Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,8 +6,8 @@ class Importer::TagsIndexImporter < Importer::BaseImporter
 | 
				
			||||||
      in_work_unit(tmp) do |tags|
 | 
					      in_work_unit(tmp) do |tags|
 | 
				
			||||||
        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: tags).bulk_body
 | 
					        bulk = Chewy::Index::Import::BulkBuilder.new(index, to_index: tags).bulk_body
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        indexed = bulk.select { |entry| entry[:index] }.size
 | 
					        indexed = bulk.count { |entry| entry[:index] }
 | 
				
			||||||
        deleted = bulk.select { |entry| entry[:delete] }.size
 | 
					        deleted = bulk.count { |entry| entry[:delete] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 | 
					        Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,21 +8,51 @@ class PermalinkRedirector
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def redirect_path
 | 
					  def redirect_path
 | 
				
			||||||
    if path_segments[0].present? && path_segments[0].start_with?('@') && path_segments[1] =~ /\d/
 | 
					    if at_username_status_request? || statuses_status_request?
 | 
				
			||||||
      find_status_url_by_id(path_segments[1])
 | 
					      find_status_url_by_id(second_segment)
 | 
				
			||||||
    elsif path_segments[0].present? && path_segments[0].start_with?('@')
 | 
					    elsif at_username_request?
 | 
				
			||||||
      find_account_url_by_name(path_segments[0])
 | 
					      find_account_url_by_name(first_segment)
 | 
				
			||||||
    elsif path_segments[0] == 'statuses' && path_segments[1] =~ /\d/
 | 
					    elsif accounts_request? && record_integer_id_request?
 | 
				
			||||||
      find_status_url_by_id(path_segments[1])
 | 
					      find_account_url_by_id(second_segment)
 | 
				
			||||||
    elsif path_segments[0] == 'accounts' && path_segments[1] =~ /\d/
 | 
					 | 
				
			||||||
      find_account_url_by_id(path_segments[1])
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def at_username_status_request?
 | 
				
			||||||
 | 
					    at_username_request? && record_integer_id_request?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def statuses_status_request?
 | 
				
			||||||
 | 
					    statuses_request? && record_integer_id_request?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def at_username_request?
 | 
				
			||||||
 | 
					    first_segment.present? && first_segment.start_with?('@')
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def statuses_request?
 | 
				
			||||||
 | 
					    first_segment == 'statuses'
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def accounts_request?
 | 
				
			||||||
 | 
					    first_segment == 'accounts'
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def record_integer_id_request?
 | 
				
			||||||
 | 
					    second_segment =~ /\d/
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def first_segment
 | 
				
			||||||
 | 
					    path_segments.first
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def second_segment
 | 
				
			||||||
 | 
					    path_segments.second
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def path_segments
 | 
					  def path_segments
 | 
				
			||||||
    @path_segments ||= @path.gsub(/\A\//, '').split('/')
 | 
					    @path_segments ||= @path.delete_prefix('/').split('/')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def find_status_url_by_id(id)
 | 
					  def find_status_url_by_id(id)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,18 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Vacuum::ImportsVacuum
 | 
				
			||||||
 | 
					  def perform
 | 
				
			||||||
 | 
					    clean_unconfirmed_imports!
 | 
				
			||||||
 | 
					    clean_old_imports!
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def clean_unconfirmed_imports!
 | 
				
			||||||
 | 
					    BulkImport.where(state: :unconfirmed).where('created_at <= ?', 10.minutes.ago).reorder(nil).in_batches.delete_all
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def clean_old_imports!
 | 
				
			||||||
 | 
					    BulkImport.where('created_at <= ?', 1.week.ago).reorder(nil).in_batches.delete_all
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue