diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 137bebc599..40dc72c12d 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -70,7 +70,7 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.4.1 + image: libretranslate/libretranslate:v1.5.2 restart: unless-stopped volumes: - lt-data:/home/libretranslate/.local diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 20f3437f4e..82a2ccbb6c 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -24,4 +24,4 @@ RAILS_ENV=development ./bin/rails db:setup RAILS_ENV=development ./bin/rails assets:precompile # Precompile assets for test -RAILS_ENV=test NODE_ENV=tests ./bin/rails assets:precompile +RAILS_ENV=test ./bin/rails assets:precompile diff --git a/.eslintrc.js b/.eslintrc.js index 1360fb87f0..5adfa3bd70 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,7 @@ -module.exports = { +// @ts-check +const { defineConfig } = require('eslint-define-config'); + +module.exports = defineConfig({ root: true, extends: [ @@ -117,7 +120,6 @@ module.exports = { 'react/jsx-uses-react': 'off', // not needed with new JSX transform 'react/jsx-wrap-multilines': 'error', 'react/no-deprecated': 'off', - 'react/no-unknown-property': 'off', 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/self-closing-comp': 'error', @@ -193,6 +195,7 @@ module.exports = { 'error', { devDependencies: [ + '.eslintrc.js', 'config/webpack/**', 'app/javascript/mastodon/performance.js', 'app/javascript/mastodon/test_setup.js', @@ -297,7 +300,6 @@ module.exports = { 'formatjs/no-id': 'off', // IDs are used for translation keys 'formatjs/no-invalid-icu': 'error', 'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings - 'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx 'formatjs/no-multiple-whitespaces': 'error', 'formatjs/no-offset': 'error', 'formatjs/no-useless-message': 'error', @@ -316,6 +318,7 @@ module.exports = { overrides: [ { files: [ + '.eslintrc.js', '*.config.js', '.*rc.js', 'ide-helper.js', @@ -366,7 +369,7 @@ module.exports = { '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], '@typescript-eslint/consistent-type-exports': 'error', '@typescript-eslint/consistent-type-imports': 'error', - "@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}], + "@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }], 'jsdoc/require-jsdoc': 'off', @@ -389,14 +392,6 @@ module.exports = { env: { jest: true, }, - }, - { - files: [ - 'streaming/**/*', - ], - rules: { - 'import/no-commonjs': 'off', - }, - }, + } ], -}; +}); diff --git a/.github/actions/setup-javascript/action.yml b/.github/actions/setup-javascript/action.yml index 00a5c46bdc..07fd4d08d3 100644 --- a/.github/actions/setup-javascript/action.yml +++ b/.github/actions/setup-javascript/action.yml @@ -9,7 +9,7 @@ runs: using: 'composite' steps: - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000000..5532c49618 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,13 @@ +coverage: + status: + project: + default: + # Github status check is not blocking + informational: true + patch: + default: + # Github status check is not blocking + informational: true +comment: + # Only write a comment in PR if there are changes + require_changes: true diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 77b64fdd3f..dab99829a1 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -22,6 +22,7 @@ 'react-hotkeys', // Requires code changes // Requires Webpacker upgrade or replacement + '@svgr/webpack', '@types/webpack', 'babel-loader', 'compression-webpack-plugin', @@ -49,7 +50,6 @@ matchManagers: ['bundler'], matchPackageNames: [ 'rack', // Needs to be synced with Rails version - 'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x 'strong_migrations', // Requires manual upgrade 'sidekiq', // Requires manual upgrade 'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version @@ -99,6 +99,16 @@ matchUpdateTypes: ['patch', 'minor'], groupName: 'eslint (non-major)', }, + { + // Group actions/*-artifact in the same PR + matchManagers: ['github-actions'], + matchPackageNames: [ + 'actions/download-artifact', + 'actions/upload-artifact', + ], + matchUpdateTypes: ['major'], + groupName: 'artifact actions (major)', + }, { // Update @types/* packages every week, with one grouped PR matchPackagePrefixes: '@types/', diff --git a/.github/workflows/build-push-pr.yml b/.github/workflows/build-push-pr.yml index b5f6cbc746..4505151e1a 100644 --- a/.github/workflows/build-push-pr.yml +++ b/.github/workflows/build-push-pr.yml @@ -29,6 +29,7 @@ jobs: needs: compute-suffix uses: ./.github/workflows/build-container-image.yml with: + file_to_build: Dockerfile platforms: linux/amd64,linux/arm64 use_native_arm64_builder: false push_to_images: | @@ -39,3 +40,19 @@ jobs: tags: | type=ref,event=pr secrets: inherit + + build-image-streaming: + needs: compute-suffix + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: streaming/Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: false + push_to_images: | + ghcr.io/${{ github.repository_owner }}/mastodon-streaming + version_metadata: ${{ needs.compute-suffix.outputs.metadata }} + flavor: | + latest=auto + tags: | + type=ref,event=pr + secrets: inherit diff --git a/.github/workflows/test-image-build.yml b/.github/workflows/test-image-build.yml index 778e341771..980e071897 100644 --- a/.github/workflows/test-image-build.yml +++ b/.github/workflows/test-image-build.yml @@ -7,6 +7,7 @@ on: - .github/workflows/build-releases.yml - .github/workflows/test-image-build.yml - Dockerfile + - streaming/Dockerfile permissions: contents: read @@ -18,4 +19,17 @@ jobs: uses: ./.github/workflows/build-container-image.yml with: + file_to_build: Dockerfile platforms: linux/amd64 # Testing only on native platform so it is performant + cache: true + + build-image-streaming: + concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-streaming + cancel-in-progress: true + + uses: ./.github/workflows/build-container-image.yml + with: + file_to_build: streaming/Dockerfile + platforms: linux/amd64 # Testing only on native platform so it is performant + cache: true diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 07fd25fb1b..ae25648a0b 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -94,7 +94,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - DISABLE_SIMPLECOV: true + DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} RAILS_ENV: test ALLOW_NOPAM: true PAM_ENABLED: true @@ -137,6 +137,12 @@ jobs: - run: bin/rspec + - name: Upload coverage reports to Codecov + if: matrix.ruby-version == '.ruby-version' + uses: codecov/codecov-action@v3 + with: + files: coverage/lcov/mastodon.lcov + test-e2e: name: End to End testing runs-on: ubuntu-latest @@ -221,7 +227,7 @@ jobs: path: tmp/screenshots/ test-search: - name: Testing search + name: Elastic Search integration testing runs-on: ubuntu-latest needs: @@ -308,7 +314,7 @@ jobs: - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' - - run: bundle exec rake spec:search + - run: bin/rspec --tag search - name: Archive logs uses: actions/upload-artifact@v3 diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index 3be2a3d490..29646f28e8 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -1,33 +1,21 @@ # This configuration was generated by # `haml-lint --auto-gen-config` -# on 2023-10-26 09:32:34 -0400 using Haml-Lint version 0.51.0. +# on 2023-12-15 11:02:19 -0500 using Haml-Lint version 0.52.0. # The point is for the user to remove these configuration records # one by one as the lints are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of Haml-Lint, may require this file to be generated again. linters: - # Offense count: 16 + # Offense count: 11 LineLength: exclude: - - 'app/views/admin/account_actions/new.html.haml' - - 'app/views/admin/accounts/index.html.haml' - - 'app/views/admin/ip_blocks/new.html.haml' - 'app/views/admin/roles/_form.html.haml' - - 'app/views/admin/settings/discovery/show.html.haml' - 'app/views/auth/registrations/edit.html.haml' - 'app/views/auth/registrations/new.html.haml' - - 'app/views/filters/_filter_fields.html.haml' - 'app/views/media/player.html.haml' - 'app/views/settings/applications/_fields.html.haml' - 'app/views/settings/imports/index.html.haml' - 'app/views/settings/preferences/appearance/show.html.haml' - 'app/views/settings/preferences/notifications/show.html.haml' - 'app/views/settings/preferences/other/show.html.haml' - - # Offense count: 9 - RuboCop: - exclude: - - 'app/views/admin/accounts/_buttons.html.haml' - - 'app/views/admin/accounts/_local_account.html.haml' - - 'app/views/admin/roles/_form.html.haml' diff --git a/.rubocop.yml b/.rubocop.yml index 68352e3198..61cb1164b4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -105,6 +105,21 @@ Rails/Exit: - 'config/boot.rb' - 'lib/mastodon/cli/*.rb' +Rails/SkipsModelValidations: + Exclude: + - 'db/*migrate/**/*' + +# Reason: We want to preserve the ability to migrate from arbitrary old versions, +# and cannot guarantee that every installation has run every migration as they upgrade. +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsunusedignoredcolumns +Rails/UnusedIgnoredColumns: + Enabled: false + +# Reason: Prevailing style choice +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsnegateinclude +Rails/NegateInclude: + Enabled: false + # Reason: Some single letter camel case files shouldn't be split # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath RSpec/FilePath: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7f68cd9b9c..030f311101 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -26,9 +26,7 @@ Lint/NonLocalExitFromIterator: # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 144 - Exclude: - - 'app/serializers/initial_state_serializer.rb' + Max: 100 # Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: @@ -109,25 +107,11 @@ Rails/ApplicationController: # Include: app/models/**/*.rb Rails/HasAndBelongsToMany: Exclude: - - 'app/models/concerns/account_associations.rb' + - 'app/models/concerns/account/associations.rb' - 'app/models/preview_card.rb' - 'app/models/status.rb' - 'app/models/tag.rb' -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/HasManyOrHasOneDependent: - Exclude: - - 'app/models/concerns/account_counters.rb' - - 'app/models/conversation.rb' - - 'app/models/custom_emoji.rb' - - 'app/models/custom_emoji_category.rb' - - 'app/models/domain_block.rb' - - 'app/models/invite.rb' - - 'app/models/status.rb' - - 'app/models/user.rb' - - 'app/models/web/push_subscription.rb' - # Configuration parameters: Include. # Include: app/controllers/**/*.rb, app/mailers/**/*.rb Rails/LexicallyScopedActionFilter: @@ -135,23 +119,6 @@ Rails/LexicallyScopedActionFilter: - 'app/controllers/auth/passwords_controller.rb' - 'app/controllers/auth/registrations_controller.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/NegateInclude: - 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/services/activitypub/process_status_update_service.rb' - - 'app/services/fetch_link_card_service.rb' - - 'app/workers/web/push_notification_worker.rb' - - 'lib/paperclip/color_extractor.rb' - Rails/OutputSafety: Exclude: - 'config/initializers/simple_form.rb' @@ -174,7 +141,7 @@ Rails/SkipsModelValidations: Exclude: - 'app/controllers/admin/invites_controller.rb' - 'app/controllers/concerns/session_tracking_concern.rb' - - 'app/models/concerns/account_merging.rb' + - 'app/models/concerns/account/merging.rb' - 'app/models/concerns/expireable.rb' - 'app/models/status.rb' - 'app/models/trends/links.rb' @@ -197,22 +164,6 @@ Rails/SkipsModelValidations: - 'app/workers/move_worker.rb' - 'app/workers/scheduler/ip_cleanup_scheduler.rb' - 'app/workers/scheduler/scheduled_statuses_scheduler.rb' - - 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb' - - 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb' - - 'db/migrate/20170209184350_add_reply_to_statuses.rb' - - 'db/migrate/20170304202101_add_type_to_media_attachments.rb' - - 'db/migrate/20180528141303_fix_accounts_unique_index.rb' - - 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb' - - 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb' - - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - - 'db/migrate/20191007013357_update_pt_locales.rb' - - 'db/migrate/20220316233212_update_kurdish_locales.rb' - - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' - - 'db/post_migrate/20200917193528_migrate_notifications_type.rb' - - 'db/post_migrate/20201017234926_fill_account_suspension_origin.rb' - - 'db/post_migrate/20220617202502_migrate_roles.rb' - - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' - - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' - 'lib/mastodon/cli/accounts.rb' - 'lib/mastodon/cli/maintenance.rb' - 'spec/lib/activitypub/activity/follow_spec.rb' @@ -228,19 +179,6 @@ Rails/UniqueValidationWithoutIndex: - 'app/models/identity.rb' - 'app/models/webauthn_credential.rb' -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/UnusedIgnoredColumns: - Exclude: - - 'app/models/account.rb' - - 'app/models/account_stat.rb' - - 'app/models/admin/action_log.rb' - - 'app/models/custom_filter.rb' - - 'app/models/email_domain_block.rb' - - 'app/models/report.rb' - - 'app/models/status_edit.rb' - - 'app/models/user.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: exists, where @@ -254,7 +192,7 @@ Rails/WhereExists: - 'app/lib/feed_manager.rb' - 'app/lib/status_cache_hydrator.rb' - 'app/lib/suspicious_sign_in_detector.rb' - - 'app/models/concerns/account_interactions.rb' + - 'app/models/concerns/account/interactions.rb' - 'app/models/featured_tag.rb' - 'app/models/poll.rb' - 'app/models/session_activation.rb' @@ -309,7 +247,7 @@ Style/FetchEnvVar: - 'config/initializers/devise.rb' - 'config/initializers/paperclip.rb' - 'config/initializers/vapid.rb' - - 'lib/mastodon/premailer_webpack_strategy.rb' + - 'lib/premailer_webpack_strategy.rb' - 'lib/mastodon/redis_config.rb' - 'lib/tasks/repo.rake' - 'spec/features/profile_spec.rb' @@ -344,8 +282,8 @@ Style/GuardClause: - 'app/lib/request_pool.rb' - 'app/lib/webfinger.rb' - 'app/lib/webfinger_resource.rb' - - 'app/models/concerns/account_counters.rb' - - 'app/models/concerns/ldap_authenticable.rb' + - 'app/models/concerns/account/counters.rb' + - 'app/models/concerns/user/ldap_authenticable.rb' - 'app/models/tag.rb' - 'app/models/user.rb' - 'app/services/fan_out_on_write_service.rb' @@ -359,8 +297,8 @@ Style/GuardClause: - 'config/initializers/devise.rb' - 'db/migrate/20170901141119_truncate_preview_cards.rb' - 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb' - - 'lib/devise/two_factor_ldap_authenticatable.rb' - - 'lib/devise/two_factor_pam_authenticatable.rb' + - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' + - 'lib/devise/strategies/two_factor_pam_authenticatable.rb' - 'lib/mastodon/cli/accounts.rb' - 'lib/mastodon/cli/maintenance.rb' - 'lib/mastodon/cli/media.rb' @@ -374,8 +312,8 @@ Style/HashAsLastArrayItem: Exclude: - 'app/controllers/admin/statuses_controller.rb' - 'app/controllers/api/v1/statuses_controller.rb' - - 'app/models/concerns/account_counters.rb' - - 'app/models/concerns/status_threading_concern.rb' + - 'app/models/concerns/account/counters.rb' + - 'app/models/concerns/status/threading_concern.rb' - 'app/models/status.rb' - 'app/services/batched_remove_status_service.rb' - 'app/services/notify_service.rb' @@ -394,22 +332,6 @@ Style/IfUnlessModifier: - 'config/initializers/devise.rb' - 'config/initializers/ffmpeg.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: InverseMethods, InverseBlocks. -Style/InverseMethods: - Exclude: - - 'app/models/custom_filter.rb' - - 'app/services/update_account_service.rb' - - 'spec/controllers/activitypub/replies_controller_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: line_count_dependent, lambda, literal -Style/Lambda: - Exclude: - - 'config/initializers/simple_form.rb' - - 'config/routes.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/MapToHash: Exclude: @@ -474,40 +396,26 @@ Style/RedundantFetchBlock: - 'config/initializers/paperclip.rb' - 'config/puma.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleReturnValues. -Style/RedundantReturn: - Exclude: - - 'app/controllers/api/v1/directories_controller.rb' - - 'app/controllers/auth/confirmations_controller.rb' - - 'app/lib/ostatus/tag_manager.rb' - - 'app/models/form/import.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! Style/SafeNavigation: Exclude: - - 'app/models/concerns/account_finder_concern.rb' + - 'app/models/concerns/account/finder_concern.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: only_raise, only_fail, semantic Style/SignalException: Exclude: - - 'lib/devise/two_factor_ldap_authenticatable.rb' - - 'lib/devise/two_factor_pam_authenticatable.rb' + - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' + - 'lib/devise/strategies/two_factor_pam_authenticatable.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Style/SingleArgumentDig: Exclude: - 'lib/webpacker/manifest_extensions.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/StderrPuts: - Exclude: - - 'config/boot.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Mode. Style/StringConcatenation: @@ -526,13 +434,6 @@ Style/StringLiterals: - 'config/initializers/webauthn.rb' - 'config/routes.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. -# AllowedMethods: define_method, mail, respond_to -Style/SymbolProc: - Exclude: - - 'config/initializers/3_omniauth.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, AllowSafeAssignment. # SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex diff --git a/.simplecov b/.simplecov new file mode 100644 index 0000000000..fbd0207bec --- /dev/null +++ b/.simplecov @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +if ENV['CI'] + require 'simplecov-lcov' + SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true + SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter +else + SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter +end + +SimpleCov.start 'rails' do + enable_coverage :branch + + add_filter 'lib/linter' + + add_group 'Libraries', 'lib' + add_group 'Policies', 'app/policies' + add_group 'Presenters', 'app/presenters' + add_group 'Serializers', 'app/serializers' + add_group 'Services', 'app/services' + add_group 'Validators', 'app/validators' +end diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a232915b6f..2271802ca9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,10 @@ You can contribute in the following ways: If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). +## API Changes and Additions + +Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation). + ## Bug reports Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected. diff --git a/Dockerfile b/Dockerfile index 2b23ea6e48..4d5bd57f11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,111 +1,259 @@ # syntax=docker/dockerfile:1.4 -# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim -ARG NODE_VERSION="20.9-bookworm-slim" -FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby -FROM node:${NODE_VERSION} as build +# Please see https://docs.docker.com/engine/reference/builder for information about +# the extended buildx capabilities used in this file. +# Make sure multiarch TARGETPLATFORM is available for interpolation +# See: https://docs.docker.com/build/building/multi-platform/ +ARG TARGETPLATFORM=${TARGETPLATFORM} +ARG BUILDPLATFORM=${BUILDPLATFORM} -COPY --link --from=ruby /opt/ruby /opt/ruby +# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.2.2"] +ARG RUBY_VERSION="3.2.2" +# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] +ARG NODE_MAJOR_VERSION="20" +# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] +ARG DEBIAN_VERSION="bookworm" +# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) +FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node +# Ruby image to use for base image based on combined variables (ex: 3.2.2-slim-bookworm) +FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby -ENV DEBIAN_FRONTEND="noninteractive" \ - PATH="${PATH}:/opt/ruby/bin" - -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -WORKDIR /opt/mastodon - -# hadolint ignore=DL3008 -RUN apt-get update && \ - apt-get -yq dist-upgrade && \ - apt-get install -y --no-install-recommends build-essential \ - git \ - libicu-dev \ - libidn-dev \ - libpq-dev \ - libjemalloc-dev \ - zlib1g-dev \ - libgdbm-dev \ - libgmp-dev \ - libssl-dev \ - libyaml-dev \ - ca-certificates \ - libreadline8 \ - python3 \ - shared-mime-info && \ - bundle config set --local deployment 'true' && \ - bundle config set --local without 'development test' && \ - bundle config set silence_root_warning true && \ - corepack enable - -COPY Gemfile* package.json yarn.lock .yarnrc.yml /opt/mastodon/ -COPY .yarn /opt/mastodon/.yarn - -RUN bundle install -j"$(nproc)" - -RUN yarn workspaces focus --all --production && \ - yarn cache clean - -FROM node:${NODE_VERSION} - -# Use those args to specify your own version flags & suffixes +# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA +# Example: v4.2.0-nightly.2023.11.09+something +# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] ARG MASTODON_VERSION_PRERELEASE="" +# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"] ARG MASTODON_VERSION_METADATA="" +# Allow Ruby on Rails to serve static files +# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files +ARG RAILS_SERVE_STATIC_FILES="true" +# Allow to use YJIT compiler +# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md +ARG RUBY_YJIT_ENABLE="1" +# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin] +ARG TZ="Etc/UTC" +# Linux UID (user id) for the mastodon user, change with [--build-arg UID=1234] ARG UID="991" +# Linux GID (group id) for the mastodon user, change with [--build-arg GID=1234] ARG GID="991" -COPY --link --from=ruby /opt/ruby /opt/ruby +# Apply Mastodon build options based on options above +ENV \ +# Apply Mastodon version information + MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ + MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \ +# Apply Mastodon static files and YJIT options + RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \ + RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \ +# Apply timezone + TZ=${TZ} -SHELL ["/bin/bash", "-o", "pipefail", "-c"] +ENV \ +# Configure the IP to bind Mastodon to when serving traffic + BIND="0.0.0.0" \ +# Use production settings for Yarn, Node and related nodejs based tools + NODE_ENV="production" \ +# Use production settings for Ruby on Rails + RAILS_ENV="production" \ +# Add Ruby and Mastodon installation to the PATH + DEBIAN_FRONTEND="noninteractive" \ + PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \ +# Optimize jemalloc 5.x performance + MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" -ENV DEBIAN_FRONTEND="noninteractive" \ - PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" +# Set default shell used for running commands +SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"] -# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use -# hadolint ignore=DL3008,DL3009 -RUN apt-get update && \ - echo "Etc/UTC" > /etc/localtime && \ - groupadd -g "${GID}" mastodon && \ - useradd -l -u "$UID" -g "${GID}" -m -d /opt/mastodon mastodon && \ - apt-get -y --no-install-recommends install whois \ - wget \ - procps \ - libssl3 \ - libpq5 \ - imagemagick \ - ffmpeg \ - libjemalloc2 \ - libicu72 \ - libidn12 \ - libyaml-0-2 \ - file \ - ca-certificates \ - tzdata \ - libreadline8 \ - tini && \ - ln -s /opt/mastodon /mastodon && \ - corepack enable +ARG TARGETPLATFORM -# Note: no, cleaning here since Debian does this automatically -# See the file /etc/apt/apt.conf.d/docker-clean within the Docker image's filesystem +RUN echo "Target platform is $TARGETPLATFORM" -COPY --chown=mastodon:mastodon . /opt/mastodon -COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon +RUN \ +# Remove automatic apt cache Docker cleanup scripts + rm -f /etc/apt/apt.conf.d/docker-clean; \ +# Sets timezone + echo "${TZ}" > /etc/localtime; \ +# Creates mastodon user/group and sets home directory + groupadd -g "${GID}" mastodon; \ + useradd -l -u "${UID}" -g "${GID}" -m -d /opt/mastodon mastodon; \ +# Creates /mastodon symlink to /opt/mastodon + ln -s /opt/mastodon /mastodon; -ENV RAILS_ENV="production" \ - NODE_ENV="production" \ - RAILS_SERVE_STATIC_FILES="true" \ - BIND="0.0.0.0" \ - MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ - MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" - -# Set the run user -USER mastodon +# Set /opt/mastodon as working directory WORKDIR /opt/mastodon -# Precompile assets -RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile +# hadolint ignore=DL3008,DL3005 +RUN \ +# Mount Apt cache and lib directories from Docker buildx caches +--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ +--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ +# Apt update & upgrade to check for security updates to Debian image + apt-get update; \ + apt-get dist-upgrade -yq; \ +# Install jemalloc, curl and other necessary components + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + ffmpeg \ + file \ + imagemagick \ + libjemalloc2 \ + patchelf \ + procps \ + tini \ + tzdata \ + ; \ +# Patch Ruby to use jemalloc + patchelf --add-needed libjemalloc.so.2 /usr/local/bin/ruby; \ +# Discard patchelf after use + apt-get purge -y \ + patchelf \ + ; -# Set the work dir and the container entry point -ENTRYPOINT ["/usr/bin/tini", "--"] -EXPOSE 3000 4000 +# Create temporary build layer from base image +FROM ruby as build + +# Copy Node package configuration files into working directory +COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ +COPY .yarn /opt/mastodon/.yarn + +COPY --from=node /usr/local/bin /usr/local/bin +COPY --from=node /usr/local/lib /usr/local/lib + +ARG TARGETPLATFORM + +# hadolint ignore=DL3008 +RUN \ +# Mount Apt cache and lib directories from Docker buildx caches +--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ +--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ +# Install build tools and bundler dependencies from APT + apt-get install -y --no-install-recommends \ + g++ \ + gcc \ + git \ + libgdbm-dev \ + libgmp-dev \ + libicu-dev \ + libidn-dev \ + libpq-dev \ + libssl-dev \ + make \ + shared-mime-info \ + zlib1g-dev \ + ; + +RUN \ +# Configure Corepack + rm /usr/local/bin/yarn*; \ + corepack enable; \ + corepack prepare --activate; + +# Create temporary bundler specific build layer from build layer +FROM build as bundler + +ARG TARGETPLATFORM + +# Copy Gemfile config into working directory +COPY Gemfile* /opt/mastodon/ + +RUN \ +# Mount Ruby Gem caches +--mount=type=cache,id=gem-cache-${TARGETPLATFORM},target=/usr/local/bundle/cache/,sharing=locked \ +# Configure bundle to prevent changes to Gemfile and Gemfile.lock + bundle config set --global frozen "true"; \ +# Configure bundle to not cache downloaded Gems + bundle config set --global cache_all "false"; \ +# Configure bundle to only process production Gems + bundle config set --local without "development test"; \ +# Configure bundle to not warn about root user + bundle config set silence_root_warning "true"; \ +# Download and install required Gems + bundle install -j"$(nproc)"; + +# Create temporary node specific build layer from build layer +FROM build as yarn + +ARG TARGETPLATFORM + +# Copy Node package configuration files into working directory +COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ +COPY streaming/package.json /opt/mastodon/streaming/ +COPY .yarn /opt/mastodon/.yarn + +# hadolint ignore=DL3008 +RUN \ +--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ +--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ +# Install Node packages + yarn workspaces focus --production @mastodon/mastodon; + +# Create temporary assets build layer from build layer +FROM build as precompiler + +# Copy Mastodon sources into precompiler layer +COPY . /opt/mastodon/ + +# Copy bundler and node packages from build layer to container +COPY --from=yarn /opt/mastodon /opt/mastodon/ +COPY --from=bundler /opt/mastodon /opt/mastodon/ +COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ + +ARG TARGETPLATFORM + +RUN \ +# Use Ruby on Rails to create Mastodon assets + OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder bundle exec rails assets:precompile; \ +# Cleanup temporary files + rm -fr /opt/mastodon/tmp; + +# Prep final Mastodon Ruby layer +FROM ruby as mastodon + +ARG TARGETPLATFORM + +# hadolint ignore=DL3008 +RUN \ +# Mount Apt cache and lib directories from Docker buildx caches +--mount=type=cache,id=apt-cache-${TARGETPLATFORM},target=/var/cache/apt,sharing=locked \ +--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ +# Mount Corepack and Yarn caches from Docker buildx caches +--mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ +--mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ +# Apt update install non-dev versions of necessary components + apt-get install -y --no-install-recommends \ + libssl3 \ + libpq5 \ + libicu72 \ + libidn12 \ + libreadline8 \ + libyaml-0-2 \ + ; + +# Copy Mastodon sources into final layer +COPY . /opt/mastodon/ + +# Copy compiled assets to layer +COPY --from=precompiler /opt/mastodon/public/packs /opt/mastodon/public/packs +COPY --from=precompiler /opt/mastodon/public/assets /opt/mastodon/public/assets +# Copy bundler components to layer +COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ + +RUN \ +# Precompile bootsnap code for faster Rails startup + bundle exec bootsnap precompile --gemfile app/ lib/; + +RUN \ +# Pre-create and chown system volume to Mastodon user + mkdir -p /opt/mastodon/public/system; \ + chown mastodon:mastodon /opt/mastodon/public/system; \ +# Set Mastodon user as owner of tmp folder + chown -R mastodon:mastodon /opt/mastodon/tmp; + +# Set the running user for resulting container +USER mastodon +# Expose default Puma ports +EXPOSE 3000 +# Set container tini as default entry point +ENTRYPOINT ["/usr/bin/tini", "--"] \ No newline at end of file diff --git a/Gemfile b/Gemfile index 47508175b6..300036804b 100644 --- a/Gemfile +++ b/Gemfile @@ -5,10 +5,13 @@ ruby '>= 3.0.0' gem 'puma', '~> 6.3' gem 'rails', '~> 7.1.1' -gem 'sprockets', '~> 3.7.2' +gem 'propshaft' gem 'thor', '~> 1.2' gem 'rack', '~> 2.2.7' +# For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182 +gem 'irb', '~> 1.8' + gem 'haml-rails', '~>2.0' gem 'pg', '~> 1.5' gem 'pghero' @@ -86,7 +89,6 @@ gem 'sidekiq-unique-jobs', '~> 7.1' gem 'sidekiq-bulk', '~> 0.2.0' gem 'simple-navigation', '~> 4.4' gem 'simple_form', '~> 5.2' -gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' gem 'stoplight', '~> 3.0.1' gem 'strong_migrations', '1.6.4' gem 'tty-prompt', '~> 0.23', require: false @@ -109,6 +111,9 @@ group :test do # RSpec progress bar formatter gem 'fuubar', '~> 2.5' + # RSpec helpers for email specs + gem 'email_spec' + # Extra RSpec extenion methods and helpers for sidekiq gem 'rspec-sidekiq', '~> 4.0' @@ -139,6 +144,7 @@ group :test do # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false + gem 'simplecov-lcov', '~> 0.8', require: false # Stub web requests for specs gem 'webmock', '~> 3.18' @@ -175,6 +181,9 @@ group :development do end group :development, :test do + # Interactive Debugging tools + gem 'debug', '~> 1.8' + # Profiling tools gem 'memory_profiler', require: false gem 'ruby-prof', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 8d9af4e970..3b33402b22 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -118,7 +118,7 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) android_key_attestation (0.3.0) @@ -130,21 +130,21 @@ GEM encryptor (~> 3.0.0) attr_required (1.0.1) awrence (1.2.1) - aws-eventstream (1.2.0) - aws-partitions (1.828.0) - aws-sdk-core (3.183.1) + aws-eventstream (1.3.0) + aws-partitions (1.857.0) + aws-sdk-core (3.188.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.71.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-kms (1.73.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.136.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-s3 (1.140.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.0) + aws-sigv4 (1.7.0) aws-eventstream (~> 1, >= 1.0.2) azure-storage-blob (2.0.3) azure-storage-common (~> 2.0) @@ -154,9 +154,9 @@ GEM faraday_middleware (~> 1.0, >= 1.0.0.rc1) net-http-persistent (~> 4.0) nokogiri (~> 1, >= 1.10.8) - base64 (0.1.1) + base64 (0.2.0) bcp47_spec (0.2.1) - bcrypt (3.1.19) + bcrypt (3.1.20) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -168,14 +168,14 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bigdecimal (3.1.4) + bigdecimal (3.1.5) bindata (2.4.15) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) blurhash (0.1.7) bootsnap (1.17.0) msgpack (~> 1.2) - brakeman (6.0.1) + brakeman (6.1.0) browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -197,7 +197,7 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.3.4) + chewy (7.4.0) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl @@ -220,6 +220,9 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.4) + debug (1.9.0) + irb (~> 1.10) + reline (>= 0.3.8) debug_inspector (1.1.0) devise (4.9.3) bcrypt (~> 3.0) @@ -242,13 +245,13 @@ GEM docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.6.6) + doorkeeper (5.6.8) railties (>= 5) dotenv (2.8.1) dotenv-rails (2.8.1) dotenv (= 2.8.1) railties (>= 3.2) - drb (2.1.1) + drb (2.2.0) ruby2_keywords ed25519 (1.3.0) elasticsearch (7.13.3) @@ -260,12 +263,16 @@ GEM elasticsearch-transport (7.13.3) faraday (~> 1) multi_json + email_spec (2.2.2) + htmlentities (~> 4.3.3) + launchy (~> 2.1) + mail (~> 2.7) encryptor (3.0.0) erubi (1.12.0) et-orbi (1.2.7) tzinfo excon (0.104.0) - fabrication (2.30.0) + fabrication (2.31.0) faker (3.2.2) i18n (>= 1.8.11, < 2) faraday (1.10.3) @@ -319,7 +326,7 @@ GEM ruby-progressbar (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - haml (6.2.0) + haml (6.3.0) temple (>= 0.8.2) thor tilt @@ -328,7 +335,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.51.0) + haml_lint (0.52.0) haml (>= 4.0) parallel (~> 1.10) rainbow @@ -370,11 +377,11 @@ GEM terminal-table (>= 1.5.1) idn-ruby (0.1.5) io-console (0.6.0) - irb (1.8.3) + irb (1.10.1) rdoc reline (>= 0.3.8) jmespath (1.6.2) - json (2.6.3) + json (2.7.1) json-canonicalization (1.0.0) json-jwt (1.15.3) activesupport (>= 4.2) @@ -432,7 +439,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.21.4) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -458,7 +465,7 @@ GEM msgpack (1.7.2) multi_json (1.15.0) multipart-post (2.3.0) - mutex_m (0.1.2) + mutex_m (0.2.0) net-http (0.4.0) uri net-http-persistent (4.0.2) @@ -474,10 +481,11 @@ GEM net-smtp (0.4.0) net-protocol nio4r (2.5.9) - nokogiri (1.15.4) + nokogiri (1.15.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.1) + oj (3.16.3) + bigdecimal (>= 3.0) omniauth (2.1.1) hashie (>= 3.4.6) rack (>= 2.2.3) @@ -507,7 +515,7 @@ GEM openssl (> 2.0) orm_adapter (0.5.0) ox (2.14.17) - parallel (1.23.0) + parallel (1.24.0) parser (3.2.2.4) ast (~> 2.4.1) racc @@ -515,7 +523,7 @@ GEM pastel (0.8.0) tty-color (~> 0.5) pg (1.5.4) - pghero (3.3.4) + pghero (3.4.0) activerecord (>= 6) posix-spawn (0.3.15) premailer (1.21.0) @@ -527,9 +535,14 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) + propshaft (0.8.0) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + railties (>= 7.0.0) psych (5.1.1.1) stringio - public_suffix (5.0.3) + public_suffix (5.0.4) puma (6.4.0) nio4r (~> 2.0) pundit (2.3.1) @@ -595,13 +608,13 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) + rake (13.1.0) rdf (3.3.1) bcp47_spec (~> 0.2) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.6.1) rdf (~> 3.2) - rdoc (6.5.0) + rdoc (6.6.1) psych (>= 4.0.0) redcarpet (3.6.0) redis (4.8.1) @@ -609,8 +622,8 @@ GEM redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.8.2) - reline (0.3.9) + regexp_parser (2.8.3) + reline (0.4.1) io-console (~> 0.5) request_store (1.5.1) rack (>= 1.4) @@ -635,7 +648,7 @@ GEM rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.3) + rspec-rails (6.1.0) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -649,7 +662,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 8) rspec-support (3.12.1) - rubocop (1.57.2) + rubocop (1.59.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -657,7 +670,7 @@ GEM rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.30.0) @@ -666,13 +679,14 @@ GEM rubocop (~> 1.41) rubocop-factory_bot (2.24.0) rubocop (~> 1.33) - rubocop-performance (1.19.1) - rubocop (>= 1.7.0, < 2.0) - rubocop-ast (>= 0.4.0) - rubocop-rails (2.22.1) + rubocop-performance (1.20.0) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-rails (2.23.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) rubocop-rspec (2.25.0) rubocop (~> 1.40) rubocop-capybara (~> 2.17) @@ -694,7 +708,7 @@ GEM scenic (1.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - selenium-webdriver (4.15.0) + selenium-webdriver (4.16.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -725,20 +739,14 @@ GEM simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) + simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) smart_properties (1.17.0) - sprockets (3.7.2) - concurrent-ruby (~> 1.0) - rack (> 1, < 3) - sprockets-rails (3.4.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - sprockets (>= 3.0.0) stackprof (0.2.25) statsd-ruby (1.5.0) stoplight (3.0.2) redlock (~> 1.0) - stringio (3.0.9) + stringio (3.1.0) strong_migrations (1.6.4) activerecord (>= 5.2) swd (1.3.0) @@ -746,12 +754,12 @@ GEM attr_required (>= 0.0.5) httpclient (>= 2.4) sysexits (1.2.0) - temple (0.10.2) + temple (0.10.3) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - test-prof (1.2.3) + test-prof (1.3.1) thor (1.3.0) tilt (2.3.0) timeout (0.4.1) @@ -845,6 +853,7 @@ DEPENDENCIES concurrent-ruby connection_pool database_cleaner-active_record + debug (~> 1.8) devise (~> 4.9) devise-two-factor (~> 4.1) devise_pam_authenticatable2 (~> 9.2) @@ -852,6 +861,7 @@ DEPENDENCIES doorkeeper (~> 5.6) dotenv-rails (~> 2.8) ed25519 (~> 1.3) + email_spec fabrication (~> 2.30) faker (~> 3.2) fast_blank (~> 1.0) @@ -869,6 +879,7 @@ DEPENDENCIES httplog (~> 1.6.2) i18n-tasks (~> 1.0) idn-ruby + irb (~> 1.8) json-ld json-ld-preloaded (~> 3.2) json-schema (~> 4.0) @@ -899,6 +910,7 @@ DEPENDENCIES posix-spawn premailer-rails private_address_check (~> 0.5) + propshaft public_suffix (~> 5.0) puma (~> 6.3) pundit (~> 2.3) @@ -936,8 +948,7 @@ DEPENDENCIES simple-navigation (~> 4.4) simple_form (~> 5.2) simplecov (~> 0.22) - sprockets (~> 3.7.2) - sprockets-rails (~> 3.4) + simplecov-lcov (~> 0.8) stackprof stoplight (~> 3.0.1) strong_migrations (= 1.6.4) diff --git a/Procfile.dev b/Procfile.dev index fbb2c2de23..f81333b04c 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,4 +1,4 @@ web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq -stream: env PORT=4000 yarn run start +stream: env PORT=4000 yarn workspace @mastodon/streaming start webpack: bin/webpack-dev-server diff --git a/Vagrantfile b/Vagrantfile index e2c66a476e..6f0f511095 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -10,7 +10,11 @@ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - sudo apt-add-repository 'deb https://dl.yarnpkg.com/debian/ stable main' # Add repo for NodeJS -curl -sL https://deb.nodesource.com/setup_16.x | sudo bash - +sudo mkdir -p /etc/apt/keyrings +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +NODE_MAJOR=20 +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list +sudo apt-get update # Add firewall rule to redirect 80 to PORT and save sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port #{ENV["PORT"]} diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index 00db257ac7..59f2f991f2 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AccountsIndex < Chewy::Index + include DatetimeClampingConcern + settings index: index_preset(refresh_interval: '30s'), analysis: { filter: { english_stop: { @@ -60,7 +62,7 @@ class AccountsIndex < Chewy::Index field(:following_count, type: 'long') field(:followers_count, type: 'long') field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties }) - field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }) + field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }) field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' } diff --git a/app/chewy/concerns/datetime_clamping_concern.rb b/app/chewy/concerns/datetime_clamping_concern.rb new file mode 100644 index 0000000000..7f176b6e54 --- /dev/null +++ b/app/chewy/concerns/datetime_clamping_concern.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module DatetimeClampingConcern + extend ActiveSupport::Concern + + MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze + MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze + + class_methods do + def clamp_date(datetime) + datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME) + end + end +end diff --git a/app/chewy/public_statuses_index.rb b/app/chewy/public_statuses_index.rb index 2f8d515106..b006e589a1 100644 --- a/app/chewy/public_statuses_index.rb +++ b/app/chewy/public_statuses_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class PublicStatusesIndex < Chewy::Index + include DatetimeClampingConcern + settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: { filter: { english_stop: { @@ -62,6 +64,6 @@ class PublicStatusesIndex < Chewy::Index field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) }) field(:language, type: 'keyword') field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties }) - field(:created_at, type: 'date') + field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) }) end end diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index 8f7ef41ecf..5bbe6b61d7 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class StatusesIndex < Chewy::Index + include DatetimeClampingConcern + settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: { filter: { english_stop: { @@ -60,6 +62,6 @@ class StatusesIndex < Chewy::Index field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by }) field(:language, type: 'keyword') field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties }) - field(:created_at, type: 'date') + field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) }) end end diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb index 5b6349a964..c99218a47f 100644 --- a/app/chewy/tags_index.rb +++ b/app/chewy/tags_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class TagsIndex < Chewy::Index + include DatetimeClampingConcern + settings index: index_preset(refresh_interval: '30s'), analysis: { analyzer: { content: { @@ -42,6 +44,6 @@ class TagsIndex < Chewy::Index field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') } field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }) field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }) - field(:last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }) + field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }) end end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 32fc378790..567edb33b1 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -18,8 +18,6 @@ class AccountsController < ApplicationController respond_to do |format| format.html do expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? - - @rss_url = rss_url end format.rss do @@ -52,7 +50,7 @@ class AccountsController < ApplicationController end def only_media_scope - Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id) + Status.joins(:media_attachments).merge(@account.media_attachments).group(:id) end def no_replies_scope @@ -84,29 +82,21 @@ class AccountsController < ApplicationController short_account_url(@account, format: 'rss') end end + helper_method :rss_url def media_requested? - request.path.split('.').first.end_with?('/media') && !tag_requested? + path_without_format.end_with?('/media') && !tag_requested? end def replies_requested? - request.path.split('.').first.end_with?('/with_replies') && !tag_requested? + path_without_format.end_with?('/with_replies') && !tag_requested? end def tag_requested? - request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) + path_without_format.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end - def cached_filtered_status_page - cache_collection_paginated_by_id( - filtered_statuses, - Status, - PAGE_SIZE, - params_slice(:max_id, :min_id, :since_id) - ) - end - - def params_slice(*keys) - params.slice(*keys).permit(*keys) + def path_without_format + request.path.split('.').first end end diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 4f36f33f47..8b6c1a4454 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -16,7 +16,7 @@ module Admin @moderation_notes = @account.targeted_moderation_notes.latest @warnings = @account.strikes.custom.latest - render template: 'admin/accounts/show' + render 'admin/accounts/show' end end diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb index 42edec15a3..37a00ad225 100644 --- a/app/controllers/admin/action_logs_controller.rb +++ b/app/controllers/admin/action_logs_controller.rb @@ -6,7 +6,7 @@ module Admin def index authorize :audit_log, :index? - @auditable_accounts = Account.where(id: Admin::ActionLog.reorder(nil).select('distinct account_id')).select(:id, :username) + @auditable_accounts = Account.where(id: Admin::ActionLog.select('distinct account_id')).select(:id, :username) end private diff --git a/app/controllers/admin/export_domain_allows_controller.rb b/app/controllers/admin/export_domain_allows_controller.rb index adfc39da21..ca88c6525e 100644 --- a/app/controllers/admin/export_domain_allows_controller.rb +++ b/app/controllers/admin/export_domain_allows_controller.rb @@ -4,7 +4,7 @@ require 'csv' module Admin class ExportDomainAllowsController < BaseController - include AdminExportControllerConcern + include Admin::ExportControllerConcern before_action :set_dummy_import!, only: [:new] diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb index 816422d4ff..433b8a1587 100644 --- a/app/controllers/admin/export_domain_blocks_controller.rb +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -4,7 +4,7 @@ require 'csv' module Admin class ExportDomainBlocksController < BaseController - include AdminExportControllerConcern + include Admin::ExportControllerConcern before_action :set_dummy_import!, only: [:new] diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb index c1297c8b99..c893802159 100644 --- a/app/controllers/admin/relays_controller.rb +++ b/app/controllers/admin/relays_controller.rb @@ -24,7 +24,7 @@ module Admin @relay.enable! redirect_to admin_relays_path else - render action: :new + render :new end end diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index 3fd815b60a..b5f04a1caa 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -26,7 +26,7 @@ module Admin @form = Admin::StatusBatchAction.new @statuses = @report.statuses.with_includes - render template: 'admin/reports/show' + render 'admin/reports/show' end end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 5712dea888..e53b22dca3 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -32,7 +32,7 @@ module Admin private def batched_ordered_status_edits - @status.edits.reorder(nil).includes(:account, status: [:account]).find_each(order: :asc) + @status.edits.includes(:account, status: [:account]).find_each(order: :asc) end helper_method :batched_ordered_status_edits diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 135c575658..98fa1897ef 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -4,9 +4,9 @@ class Api::BaseController < ApplicationController DEFAULT_STATUSES_LIMIT = 20 DEFAULT_ACCOUNTS_LIMIT = 40 - include RateLimitHeaders - include AccessTokenTrackingConcern - include ApiCachingConcern + include Api::RateLimitHeaders + include Api::AccessTokenTrackingConcern + include Api::CachingConcern include Api::ContentSecurityPolicy skip_before_action :require_functional!, unless: :limited_federation_mode? @@ -64,7 +64,7 @@ class Api::BaseController < ApplicationController end def doorkeeper_unauthorized_render_options(error: nil) - { json: { error: (error.try(:description) || 'Not authorized') } } + { json: { error: error.try(:description) || 'Not authorized' } } end def doorkeeper_forbidden_render_options(*) @@ -105,7 +105,11 @@ class Api::BaseController < ApplicationController end def require_not_suspended! - render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.suspended? + render json: { error: 'Your login is currently disabled' }, status: 403 if current_user&.account&.unavailable? + end + + def require_valid_pagination_options! + render json: { error: 'Pagination values for `offset` and `limit` must be positive' }, status: 400 if pagination_options_invalid? end def require_user! @@ -136,6 +140,10 @@ class Api::BaseController < ApplicationController private + def pagination_options_invalid? + params.slice(:limit, :offset).values.map(&:to_i).any?(&:negative?) + end + def respond_with_error(code) render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 76ba758245..8f31336b9f 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -16,6 +16,8 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController current_user.update(user_params) if user_params ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer + rescue ActiveRecord::RecordInvalid => e + render json: ValidationErrorFormatter.new(e).as_json, status: 422 end private diff --git a/app/controllers/api/v1/accounts/familiar_followers_controller.rb b/app/controllers/api/v1/accounts/familiar_followers_controller.rb index b0bd8018a2..a49eb2eb27 100644 --- a/app/controllers/api/v1/accounts/familiar_followers_controller.rb +++ b/app/controllers/api/v1/accounts/familiar_followers_controller.rb @@ -12,7 +12,7 @@ class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController private def set_accounts - @accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections').index_by(&:id).values_at(*account_ids).compact + @accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections') end def familiar_followers diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 1a996d362a..21b1095f18 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -26,7 +26,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def hide_results? - @account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.unavailable? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 6e6ebae43b..1db521f79c 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -26,7 +26,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def hide_results? - @account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) + @account.unavailable? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account)) end def default_accounts diff --git a/app/controllers/api/v1/accounts/notes_controller.rb b/app/controllers/api/v1/accounts/notes_controller.rb index 032e807d11..6d115631a2 100644 --- a/app/controllers/api/v1/accounts/notes_controller.rb +++ b/app/controllers/api/v1/accounts/notes_controller.rb @@ -25,6 +25,6 @@ class Api::V1::Accounts::NotesController < Api::BaseController end def relationships_presenter - AccountRelationshipsPresenter.new([@account.id], current_user.account_id) + AccountRelationshipsPresenter.new([@account], current_user.account_id) end end diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb index 73f845c614..0eb13c048c 100644 --- a/app/controllers/api/v1/accounts/pins_controller.rb +++ b/app/controllers/api/v1/accounts/pins_controller.rb @@ -25,6 +25,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController end def relationships_presenter - AccountRelationshipsPresenter.new([@account.id], current_user.account_id) + AccountRelationshipsPresenter.new([@account], current_user.account_id) end end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 320084efb5..d43832177a 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,11 +5,8 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - scope = Account.where(id: account_ids).select('id') - scope.merge!(Account.without_suspended) unless truthy_param?(:with_suspended) - # .where doesn't guarantee that our results are in the same order - # we requested them, so return the "right" order to the requestor. - @accounts = scope.index_by(&:id).values_at(*account_ids).compact + @accounts = Account.where(id: account_ids).select(:id, :domain) + @accounts.merge!(Account.without_suspended) unless truthy_param?(:with_suspended) render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 51f541bd23..fe4279302f 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -19,7 +19,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - @account.suspended? ? [] : cached_account_statuses + @account.unavailable? ? [] : cached_account_statuses end def cached_account_statuses diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 653529316f..23fc85b475 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -49,7 +49,7 @@ class Api::V1::AccountsController < Api::BaseController end def mute - MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration]&.to_i || 0)) + MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: params[:duration].to_i) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end @@ -88,7 +88,7 @@ class Api::V1::AccountsController < Api::BaseController end def relationships(**options) - AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options) + AccountRelationshipsPresenter.new([@account], current_user.account_id, **options) end def account_params diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index 35c504a7ff..e79b20ce42 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -12,7 +12,7 @@ class Api::V1::DirectoriesController < Api::BaseController private def require_enabled! - return not_found unless Setting.profile_directory + not_found unless Setting.profile_directory end def set_accounts diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 7c197ce6ba..ee717ebbcc 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -25,11 +25,11 @@ class Api::V1::FollowRequestsController < Api::BaseController private def account - Account.find(params[:id]) + @account ||= Account.find(params[:id]) end def relationships(**options) - AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options) + AccountRelationshipsPresenter.new([account], current_user.account_id, **options) end def load_accounts diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index 4c17bd79c2..06e4fd8b8f 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -3,6 +3,8 @@ class Api::V1::Instances::ActivityController < Api::V1::Instances::BaseController before_action :require_enabled_api! + WEEKS_OF_ACTIVITY = 12 + def show cache_even_if_authenticated! render_with_cache json: :activity, expires_in: 1.day @@ -11,23 +13,40 @@ class Api::V1::Instances::ActivityController < Api::V1::Instances::BaseControlle private def activity - statuses_tracker = ActivityTracker.new('activity:statuses:local', :basic) - logins_tracker = ActivityTracker.new('activity:logins', :unique) - registrations_tracker = ActivityTracker.new('activity:accounts:local', :basic) - - (0...12).map do |i| - start_of_week = i.weeks.ago - end_of_week = start_of_week + 6.days - - { - week: start_of_week.to_i.to_s, - statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s, - logins: logins_tracker.sum(start_of_week, end_of_week).to_s, - registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s, - } + activity_weeks.map do |weeks_ago| + activity_json(*week_edge_days(weeks_ago)) end end + def activity_json(start_of_week, end_of_week) + { + week: start_of_week.to_i.to_s, + statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s, + logins: logins_tracker.sum(start_of_week, end_of_week).to_s, + registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s, + } + end + + def activity_weeks + 0...WEEKS_OF_ACTIVITY + end + + def week_edge_days(num) + [num.weeks.ago, num.weeks.ago + 6.days] + end + + def statuses_tracker + ActivityTracker.new('activity:statuses:local', :basic) + end + + def logins_tracker + ActivityTracker.new('activity:logins', :unique) + end + + def registrations_tracker + ActivityTracker.new('activity:accounts:local', :basic) + end + def require_enabled_api! head 404 unless Setting.activity_api_enabled && !limited_federation_mode? end diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index 8fb90305ad..7ec94312f4 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -19,7 +19,19 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr private def require_enabled_api! - head 404 unless Setting.show_domain_blocks == 'all' || (Setting.show_domain_blocks == 'users' && user_signed_in?) + head 404 unless api_enabled? + end + + def api_enabled? + show_domain_blocks_for_all? || show_domain_blocks_to_user? + end + + def show_domain_blocks_for_all? + Setting.show_domain_blocks == 'all' + end + + def show_domain_blocks_to_user? + Setting.show_domain_blocks == 'users' && user_signed_in? end def set_domain_blocks diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index dcb21ef043..e381ea2c67 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -11,6 +11,6 @@ class Api::V1::Statuses::HistoriesController < Api::V1::Statuses::BaseController private def status_edits - @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] + @status.edits.ordered.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] end end diff --git a/app/controllers/api/v1/timelines/base_controller.rb b/app/controllers/api/v1/timelines/base_controller.rb new file mode 100644 index 0000000000..173e173cc9 --- /dev/null +++ b/app/controllers/api/v1/timelines/base_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Api::V1::Timelines::BaseController < Api::BaseController + after_action :insert_pagination_headers, unless: -> { @statuses.empty? } + + private + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id + end + + def next_path_params + permitted_params.merge(max_id: pagination_max_id) + end + + def prev_path_params + permitted_params.merge(min_id: pagination_since_id) + end + + def permitted_params + params + .slice(*self.class::PERMITTED_PARAMS) + .permit(*self.class::PERMITTED_PARAMS) + end +end diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index 83b8cb4c66..36fdbea647 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -class Api::V1::Timelines::HomeController < Api::BaseController +class Api::V1::Timelines::HomeController < Api::V1::Timelines::BaseController before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] before_action :require_user!, only: [:show] - after_action :insert_pagination_headers, unless: -> { @statuses.empty? } + + PERMITTED_PARAMS = %i(local limit).freeze def show with_read_replica do @@ -40,27 +41,11 @@ class Api::V1::Timelines::HomeController < Api::BaseController HomeFeed.new(current_account) end - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def pagination_params(core_params) - params.slice(:local, :limit).permit(:local, :limit).merge(core_params) - end - def next_path - api_v1_timelines_home_url pagination_params(max_id: pagination_max_id) + api_v1_timelines_home_url next_path_params end def prev_path - api_v1_timelines_home_url pagination_params(min_id: pagination_since_id) - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id + api_v1_timelines_home_url prev_path_params end end diff --git a/app/controllers/api/v1/timelines/list_controller.rb b/app/controllers/api/v1/timelines/list_controller.rb index a15eae468d..14b884ecd9 100644 --- a/app/controllers/api/v1/timelines/list_controller.rb +++ b/app/controllers/api/v1/timelines/list_controller.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -class Api::V1::Timelines::ListController < Api::BaseController +class Api::V1::Timelines::ListController < Api::V1::Timelines::BaseController before_action -> { doorkeeper_authorize! :read, :'read:lists' } before_action :require_user! before_action :set_list before_action :set_statuses - after_action :insert_pagination_headers, unless: -> { @statuses.empty? } + PERMITTED_PARAMS = %i(limit).freeze def show render json: @statuses, @@ -41,27 +41,11 @@ class Api::V1::Timelines::ListController < Api::BaseController ListFeed.new(@list) end - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def pagination_params(core_params) - params.slice(:limit).permit(:limit).merge(core_params) - end - def next_path - api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id) + api_v1_timelines_list_url params[:id], next_path_params end def prev_path - api_v1_timelines_list_url params[:id], pagination_params(min_id: pagination_since_id) - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id + api_v1_timelines_list_url params[:id], prev_path_params end end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 6af504ff63..5bc8de8334 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -class Api::V1::Timelines::PublicController < Api::BaseController +class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController before_action :require_user!, only: [:show], if: :require_auth? - after_action :insert_pagination_headers, unless: -> { @statuses.empty? } + + PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze def show cache_if_unauthenticated! @@ -45,27 +46,11 @@ class Api::V1::Timelines::PublicController < Api::BaseController ) end - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def pagination_params(core_params) - params.slice(:local, :remote, :limit, :only_media, :allow_local_only).permit(:local, :remote, :limit, :only_media, :allow_local_only).merge(core_params) - end - def next_path - api_v1_timelines_public_url pagination_params(max_id: pagination_max_id) + api_v1_timelines_public_url next_path_params end def prev_path - api_v1_timelines_public_url pagination_params(min_id: pagination_since_id) - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id + api_v1_timelines_public_url prev_path_params end end diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index a79d65c124..4ba439dbb2 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -class Api::V1::Timelines::TagController < Api::BaseController +class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth? before_action :load_tag - after_action :insert_pagination_headers, unless: -> { @statuses.empty? } + + PERMITTED_PARAMS = %i(local limit only_media).freeze def show cache_if_unauthenticated! @@ -51,27 +52,11 @@ class Api::V1::Timelines::TagController < Api::BaseController ) end - def insert_pagination_headers - set_pagination_headers(next_path, prev_path) - end - - def pagination_params(core_params) - params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params) - end - def next_path - api_v1_timelines_tag_url params[:id], pagination_params(max_id: pagination_max_id) + api_v1_timelines_tag_url params[:id], next_path_params end def prev_path - api_v1_timelines_tag_url params[:id], pagination_params(min_id: pagination_since_id) - end - - def pagination_max_id - @statuses.last.id - end - - def pagination_since_id - @statuses.first.id + api_v1_timelines_tag_url params[:id], prev_path_params end end diff --git a/app/controllers/api/v2/media_controller.rb b/app/controllers/api/v2/media_controller.rb index 72bc694421..36c15165da 100644 --- a/app/controllers/api/v2/media_controller.rb +++ b/app/controllers/api/v2/media_controller.rb @@ -2,12 +2,22 @@ class Api::V2::MediaController < Api::V1::MediaController def create - @media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params)) - render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200 + @media_attachment = current_account.media_attachments.create!(media_and_delay_params) + render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_from_media_processing rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 rescue Paperclip::Error => e Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end + + private + + def media_and_delay_params + { delay_processing: true }.merge(media_attachment_params) + end + + def status_from_media_processing + @media_attachment.not_processed? ? 202 : 200 + end end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index cc74db58e5..0cc0f1f990 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -8,6 +8,12 @@ class Api::V2::SearchController < Api::BaseController before_action -> { authorize_if_got_token! :read, :'read:search' } before_action :validate_search_params! + with_options unless: :user_signed_in? do + before_action :query_pagination_error, if: :pagination_requested? + before_action :remote_resolve_error, if: :remote_resolve_requested? + end + before_action :require_valid_pagination_options! + def index @search = Search.new(search_results) render json: @search, serializer: REST::SearchSerializer @@ -21,12 +27,22 @@ class Api::V2::SearchController < Api::BaseController def validate_search_params! params.require(:q) + end - return if user_signed_in? + def query_pagination_error + render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 + end - return render json: { error: 'Search queries pagination is not supported without authentication' }, status: 401 if params[:offset].present? + def remote_resolve_error + render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 + end - render json: { error: 'Search queries that resolve remote resources are not supported without authentication' }, status: 401 if truthy_param?(:resolve) + def remote_resolve_requested? + truthy_param?(:resolve) + end + + def pagination_requested? + params[:offset].present? end def search_results @@ -34,7 +50,15 @@ class Api::V2::SearchController < Api::BaseController params[:q], current_account, limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following)) + combined_search_params + ) + end + + def combined_search_params + search_params.merge( + resolve: truthy_param?(:resolve), + exclude_unreviewed: truthy_param?(:exclude_unreviewed), + following: truthy_param?(:following) ) end diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 5167928e93..167d16fc4d 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -3,37 +3,13 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController before_action :require_user! before_action :set_push_subscription, only: :update + before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions? + after_action :update_session_with_subscription, only: :create def create - active_session = current_session + @push_subscription = ::Web::PushSubscription.create!(web_push_subscription_params) - unless active_session.web_push_subscription.nil? - active_session.web_push_subscription.destroy! - active_session.update!(web_push_subscription: nil) - end - - # Mobile devices do not support regular notifications, so we enable push notifications by default - alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet? - - data = { - policy: 'all', - alerts: Notification::TYPES.index_with { alerts_enabled }, - } - - data.deep_merge!(data_params) if params[:data] - - push_subscription = ::Web::PushSubscription.create!( - endpoint: subscription_params[:endpoint], - key_p256dh: subscription_params[:keys][:p256dh], - key_auth: subscription_params[:keys][:auth], - data: data, - user_id: active_session.user_id, - access_token_id: active_session.access_token_id - ) - - active_session.update!(web_push_subscription: push_subscription) - - render json: push_subscription, serializer: REST::WebPushSubscriptionSerializer + render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end def update @@ -43,6 +19,41 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController private + def active_session + @active_session ||= current_session + end + + def destroy_previous_subscriptions + active_session.web_push_subscription.destroy! + active_session.update!(web_push_subscription: nil) + end + + def prior_subscriptions? + active_session.web_push_subscription.present? + end + + def subscription_data + default_subscription_data.tap do |data| + data.deep_merge!(data_params) if params[:data] + end + end + + def default_subscription_data + { + policy: 'all', + alerts: Notification::TYPES.index_with { alerts_enabled }, + } + end + + def alerts_enabled + # Mobile devices do not support regular notifications, so we enable push notifications by default + active_session.detection.device.mobile? || active_session.detection.device.tablet? + end + + def update_session_with_subscription + active_session.update!(web_push_subscription: @push_subscription) + end + def set_push_subscription @push_subscription = ::Web::PushSubscription.find(params[:id]) end @@ -51,6 +62,17 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController @subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) end + def web_push_subscription_params + { + access_token_id: active_session.access_token_id, + data: subscription_data, + endpoint: subscription_params[:endpoint], + key_auth: subscription_params[:keys][:auth], + key_p256dh: subscription_params[:keys][:p256dh], + user_id: active_session.user_id, + } + end + def data_params @data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES) end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 5f6d9c4d8d..5d476d0440 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Auth::ConfirmationsController < Devise::ConfirmationsController - include CaptchaConcern + include Auth::CaptchaConcern layout 'auth' @@ -63,7 +63,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController end def captcha_user_bypass? - return true if @confirmation_user.nil? || @confirmation_user.confirmed? + @confirmation_user.nil? || @confirmation_user.confirmed? end def set_pack diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 126e72e1ac..838869b2ee 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -2,7 +2,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController include RegistrationHelper - include RegistrationSpamConcern + include Auth::RegistrationSpamConcern layout :determine_layout @@ -125,7 +125,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def require_not_suspended! - forbidden if current_account.suspended? + forbidden if current_account.unavailable? end def set_rules diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index e387329cf5..1c773511b4 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -11,7 +11,7 @@ class Auth::SessionsController < Devise::SessionsController prepend_before_action :set_pack prepend_before_action :check_suspicious!, only: [:create] - include TwoFactorAuthenticationConcern + include Auth::TwoFactorAuthenticationConcern before_action :set_body_classes diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 3fc0938bfc..2b132417f7 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -34,8 +34,8 @@ module AccountOwnedConcern end def check_account_suspension - if @account.suspended_permanently? - permanent_suspension_response + if @account.permanently_unavailable? + permanent_unavailability_response elsif @account.suspended? && !skip_temporary_suspension_response? temporary_suspension_response end @@ -45,7 +45,7 @@ module AccountOwnedConcern false end - def permanent_suspension_response + def permanent_unavailability_response expires_in(3.minutes, public: true) gone end diff --git a/app/controllers/concerns/admin_export_controller_concern.rb b/app/controllers/concerns/admin/export_controller_concern.rb similarity index 92% rename from app/controllers/concerns/admin_export_controller_concern.rb rename to app/controllers/concerns/admin/export_controller_concern.rb index 4ac48a04b7..6228ae67fe 100644 --- a/app/controllers/concerns/admin_export_controller_concern.rb +++ b/app/controllers/concerns/admin/export_controller_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module AdminExportControllerConcern +module Admin::ExportControllerConcern extend ActiveSupport::Concern private diff --git a/app/controllers/concerns/access_token_tracking_concern.rb b/app/controllers/concerns/api/access_token_tracking_concern.rb similarity index 92% rename from app/controllers/concerns/access_token_tracking_concern.rb rename to app/controllers/concerns/api/access_token_tracking_concern.rb index cf60cfb995..bc6ae51c77 100644 --- a/app/controllers/concerns/access_token_tracking_concern.rb +++ b/app/controllers/concerns/api/access_token_tracking_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module AccessTokenTrackingConcern +module Api::AccessTokenTrackingConcern extend ActiveSupport::Concern ACCESS_TOKEN_UPDATE_FREQUENCY = 24.hours.freeze diff --git a/app/controllers/concerns/api_caching_concern.rb b/app/controllers/concerns/api/caching_concern.rb similarity index 93% rename from app/controllers/concerns/api_caching_concern.rb rename to app/controllers/concerns/api/caching_concern.rb index 12264d514e..55d7fe56d7 100644 --- a/app/controllers/concerns/api_caching_concern.rb +++ b/app/controllers/concerns/api/caching_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module ApiCachingConcern +module Api::CachingConcern extend ActiveSupport::Concern def cache_if_unauthenticated! diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/api/rate_limit_headers.rb similarity index 98% rename from app/controllers/concerns/rate_limit_headers.rb rename to app/controllers/concerns/api/rate_limit_headers.rb index 5b83d8575b..fe57b6f6bd 100644 --- a/app/controllers/concerns/rate_limit_headers.rb +++ b/app/controllers/concerns/api/rate_limit_headers.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module RateLimitHeaders +module Api::RateLimitHeaders extend ActiveSupport::Concern class_methods do diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/auth/captcha_concern.rb similarity index 98% rename from app/controllers/concerns/captcha_concern.rb rename to app/controllers/concerns/auth/captcha_concern.rb index 170c8f5e03..cfd93978ce 100644 --- a/app/controllers/concerns/captcha_concern.rb +++ b/app/controllers/concerns/auth/captcha_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module CaptchaConcern +module Auth::CaptchaConcern extend ActiveSupport::Concern include Hcaptcha::Adapters::ViewMethods diff --git a/app/controllers/concerns/registration_spam_concern.rb b/app/controllers/concerns/auth/registration_spam_concern.rb similarity index 81% rename from app/controllers/concerns/registration_spam_concern.rb rename to app/controllers/concerns/auth/registration_spam_concern.rb index af434c985a..9f4798b537 100644 --- a/app/controllers/concerns/registration_spam_concern.rb +++ b/app/controllers/concerns/auth/registration_spam_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module RegistrationSpamConcern +module Auth::RegistrationSpamConcern extend ActiveSupport::Concern def set_registration_form_time diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/auth/two_factor_authentication_concern.rb similarity index 98% rename from app/controllers/concerns/two_factor_authentication_concern.rb rename to app/controllers/concerns/auth/two_factor_authentication_concern.rb index e15b821007..ebd6a93441 100644 --- a/app/controllers/concerns/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/auth/two_factor_authentication_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module TwoFactorAuthenticationConcern +module Auth::TwoFactorAuthenticationConcern extend ActiveSupport::Concern included do diff --git a/app/controllers/concerns/challengable_concern.rb b/app/controllers/concerns/challengable_concern.rb index 2995a25e09..09874fb405 100644 --- a/app/controllers/concerns/challengable_concern.rb +++ b/app/controllers/concerns/challengable_concern.rb @@ -43,7 +43,7 @@ module ChallengableConcern def render_challenge @body_classes = 'lighter' - render template: 'auth/challenges/new', layout: 'auth' + render 'auth/challenges/new', layout: 'auth' end def challenge_passed? diff --git a/app/controllers/concerns/export_controller_concern.rb b/app/controllers/concerns/settings/export_controller_concern.rb similarity index 93% rename from app/controllers/concerns/export_controller_concern.rb rename to app/controllers/concerns/settings/export_controller_concern.rb index e1792fd6bf..2cf28cced8 100644 --- a/app/controllers/concerns/export_controller_concern.rb +++ b/app/controllers/concerns/settings/export_controller_concern.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module ExportControllerConcern +module Settings::ExportControllerConcern extend ActiveSupport::Concern included do diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb index eefd92b5a8..98b58d2117 100644 --- a/app/controllers/disputes/appeals_controller.rb +++ b/app/controllers/disputes/appeals_controller.rb @@ -11,7 +11,7 @@ class Disputes::AppealsController < Disputes::BaseController redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg') rescue ActiveRecord::RecordInvalid => e @appeal = e.record - render template: 'disputes/strikes/show' + render 'disputes/strikes/show' end private diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 180ddf070b..1b19269b23 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -26,7 +26,7 @@ class FiltersController < ApplicationController if @filter.save redirect_to filters_path else - render action: :new + render :new end end @@ -34,7 +34,7 @@ class FiltersController < ApplicationController if @filter.update(resource_params) redirect_to filters_path else - render action: :edit + render :edit end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 0a1df55066..778912c948 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -36,7 +36,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio end def require_not_suspended! - forbidden if current_account.suspended? + forbidden if current_account.unavailable? end def set_cache_headers diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index f83098f731..e6e7c24752 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -34,7 +34,7 @@ class RelationshipsController < ApplicationController end def set_relationships - @relationships = AccountRelationshipsPresenter.new(@accounts.pluck(:id), current_user.account_id) + @relationships = AccountRelationshipsPresenter.new(@accounts, current_user.account_id) end def form_account_batch_params diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index 56aeb49aa0..4d3d6e6a1b 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -23,6 +23,6 @@ class Settings::BaseController < ApplicationController end def require_not_suspended! - forbidden if current_account.suspended? + forbidden if current_account.unavailable? end end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index bb096567a9..16c201b6b3 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -25,7 +25,7 @@ class Settings::DeletesController < Settings::BaseController end def require_not_suspended! - forbidden if current_account.suspended? + forbidden if current_account.unavailable? end def challenge_passed? diff --git a/app/controllers/settings/exports/blocked_accounts_controller.rb b/app/controllers/settings/exports/blocked_accounts_controller.rb index 2190caa361..906564a3dc 100644 --- a/app/controllers/settings/exports/blocked_accounts_controller.rb +++ b/app/controllers/settings/exports/blocked_accounts_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class BlockedAccountsController < BaseController - include ExportControllerConcern + include Settings::ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/blocked_domains_controller.rb b/app/controllers/settings/exports/blocked_domains_controller.rb index bee4b2431e..09dc52392f 100644 --- a/app/controllers/settings/exports/blocked_domains_controller.rb +++ b/app/controllers/settings/exports/blocked_domains_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class BlockedDomainsController < BaseController - include ExportControllerConcern + include Settings::ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/bookmarks_controller.rb b/app/controllers/settings/exports/bookmarks_controller.rb index c12e2f147a..0321565b97 100644 --- a/app/controllers/settings/exports/bookmarks_controller.rb +++ b/app/controllers/settings/exports/bookmarks_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class BookmarksController < BaseController - include ExportControllerConcern + include Settings::ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/following_accounts_controller.rb b/app/controllers/settings/exports/following_accounts_controller.rb index acefcb15da..0ac9031fb9 100644 --- a/app/controllers/settings/exports/following_accounts_controller.rb +++ b/app/controllers/settings/exports/following_accounts_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class FollowingAccountsController < BaseController - include ExportControllerConcern + include Settings::ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/lists_controller.rb b/app/controllers/settings/exports/lists_controller.rb index bc65f56a0e..d90c71e248 100644 --- a/app/controllers/settings/exports/lists_controller.rb +++ b/app/controllers/settings/exports/lists_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class ListsController < BaseController - include ExportControllerConcern + include Settings::ExportControllerConcern def index send_export_file diff --git a/app/controllers/settings/exports/muted_accounts_controller.rb b/app/controllers/settings/exports/muted_accounts_controller.rb index 50b7bf1f79..e4b1158902 100644 --- a/app/controllers/settings/exports/muted_accounts_controller.rb +++ b/app/controllers/settings/exports/muted_accounts_controller.rb @@ -3,7 +3,7 @@ module Settings module Exports class MutedAccountsController < BaseController - include ExportControllerConcern + include Settings::ExportControllerConcern def index send_export_file diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 3ed1860a00..88cf26d74d 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -15,7 +15,7 @@ class StatusesCleanupController < ApplicationController if @policy.update(resource_params) redirect_to statuses_cleanup_path, notice: I18n.t('generic.changes_saved_msg') else - render action: :show + render :show end rescue ActionController::ParameterMissing # Do nothing diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 4748940f7c..364fbf8a18 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -42,7 +42,7 @@ module WellKnown end def check_account_suspension - gone if @account.suspended_permanently? + gone if @account.permanently_unavailable? end def gone diff --git a/app/helpers/admin/account_actions_helper.rb b/app/helpers/admin/account_actions_helper.rb new file mode 100644 index 0000000000..e132680a68 --- /dev/null +++ b/app/helpers/admin/account_actions_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Admin::AccountActionsHelper + def account_action_type_label(type) + safe_join( + [ + I18n.t("simple_form.labels.admin_account_action.types.#{type}"), + content_tag(:span, I18n.t("simple_form.hints.admin_account_action.types.#{type}"), class: 'hint'), + ] + ) + end +end diff --git a/app/helpers/admin/accounts_helper.rb b/app/helpers/admin/accounts_helper.rb new file mode 100644 index 0000000000..a936797e88 --- /dev/null +++ b/app/helpers/admin/accounts_helper.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Admin::AccountsHelper + def admin_accounts_moderation_options + [ + [t('admin.accounts.moderation.active'), 'active'], + [t('admin.accounts.moderation.silenced'), 'silenced'], + [t('admin.accounts.moderation.disabled'), 'disabled'], + [t('admin.accounts.moderation.suspended'), 'suspended'], + [safe_join([t('admin.accounts.moderation.pending'), "(#{pending_user_count_label})"], ' '), 'pending'], + ] + end + + private + + def pending_user_count_label + number_with_delimiter User.pending.count + end +end diff --git a/app/helpers/admin/ip_blocks_helper.rb b/app/helpers/admin/ip_blocks_helper.rb new file mode 100644 index 0000000000..4aae3aae7a --- /dev/null +++ b/app/helpers/admin/ip_blocks_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Admin::IpBlocksHelper + def ip_blocks_severity_label(severity) + safe_join( + [ + I18n.t("simple_form.labels.ip_block.severities.#{severity}"), + content_tag(:span, I18n.t("simple_form.hints.ip_block.severities.#{severity}"), class: 'hint'), + ] + ) + end +end diff --git a/app/helpers/admin/roles_helper.rb b/app/helpers/admin/roles_helper.rb new file mode 100644 index 0000000000..7b4702e268 --- /dev/null +++ b/app/helpers/admin/roles_helper.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Admin + module RolesHelper + def privilege_label(privilege) + safe_join( + [ + t("admin.roles.privileges.#{privilege}"), + content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint'), + ] + ) + end + + def disable_permissions?(permissions) + permissions.filter { |privilege| role_flag_value(privilege).zero? } + end + + private + + def role_flag_value(privilege) + UserRole::FLAGS[privilege] & current_user.role.computed_permissions + end + end +end diff --git a/app/helpers/admin/settings/discovery_helper.rb b/app/helpers/admin/settings/discovery_helper.rb new file mode 100644 index 0000000000..0aa4d4368f --- /dev/null +++ b/app/helpers/admin/settings/discovery_helper.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Admin::Settings::DiscoveryHelper + def discovery_warning_hint_text + authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil + end + + def discovery_hint_text + t('admin.settings.security.authorized_fetch_hint') + end + + def discovery_recommended_value + authorized_fetch_overridden? ? :overridden : nil + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fd54301b9f..daa27195e9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -110,11 +110,11 @@ module ApplicationHelper def can?(action, record) return false if record.nil? - policy(record).public_send("#{action}?") + policy(record).public_send(:"#{action}?") end def fa_icon(icon, attributes = {}) - class_names = attributes[:class]&.split(' ') || [] + class_names = attributes[:class]&.split || [] class_names << 'fa' class_names += icon.split.map { |cl| "fa-#{cl}" } diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb new file mode 100644 index 0000000000..22a1c172de --- /dev/null +++ b/app/helpers/filters_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module FiltersHelper + def filter_action_label(action) + safe_join( + [ + t("simple_form.labels.filters.actions.#{action}"), + content_tag(:span, t("simple_form.hints.filters.actions.#{action}"), class: 'hint'), + ] + ) + end +end diff --git a/app/javascript/flavours/glitch/actions/account_notes.ts b/app/javascript/flavours/glitch/actions/account_notes.ts index dbe9ee2a9f..1fb683e0d3 100644 --- a/app/javascript/flavours/glitch/actions/account_notes.ts +++ b/app/javascript/flavours/glitch/actions/account_notes.ts @@ -1,3 +1,4 @@ +import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships'; import { createAppAsyncThunk } from 'flavours/glitch/store/typed_functions'; import api from '../api'; @@ -5,8 +6,7 @@ import api from '../api'; export const submitAccountNote = createAppAsyncThunk( 'account_note/submit', async (args: { id: string; value: string }, { getState }) => { - // TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged - const response = await api(getState).post( + const response = await api(getState).post( `/api/v1/accounts/${args.id}/note`, { comment: args.value, diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index bb03e68de8..a93c027def 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -1,5 +1,15 @@ import api, { getLinks } from '../api'; +import { + followAccountSuccess, unfollowAccountSuccess, + authorizeFollowRequestSuccess, rejectFollowRequestSuccess, + followAccountRequest, followAccountFail, + unfollowAccountRequest, unfollowAccountFail, + muteAccountSuccess, unmuteAccountSuccess, + blockAccountSuccess, unblockAccountSuccess, + pinAccountSuccess, unpinAccountSuccess, + fetchRelationshipsSuccess, +} from './accounts_typed'; import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; @@ -10,36 +20,22 @@ export const ACCOUNT_LOOKUP_REQUEST = 'ACCOUNT_LOOKUP_REQUEST'; export const ACCOUNT_LOOKUP_SUCCESS = 'ACCOUNT_LOOKUP_SUCCESS'; export const ACCOUNT_LOOKUP_FAIL = 'ACCOUNT_LOOKUP_FAIL'; -export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST'; -export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; -export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL'; - -export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST'; -export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS'; -export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL'; - export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST'; -export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS'; export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL'; export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; -export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL'; export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; -export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL'; export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; -export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL'; export const ACCOUNT_PIN_REQUEST = 'ACCOUNT_PIN_REQUEST'; -export const ACCOUNT_PIN_SUCCESS = 'ACCOUNT_PIN_SUCCESS'; export const ACCOUNT_PIN_FAIL = 'ACCOUNT_PIN_FAIL'; export const ACCOUNT_UNPIN_REQUEST = 'ACCOUNT_UNPIN_REQUEST'; -export const ACCOUNT_UNPIN_SUCCESS = 'ACCOUNT_UNPIN_SUCCESS'; export const ACCOUNT_UNPIN_FAIL = 'ACCOUNT_UNPIN_FAIL'; export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; @@ -59,7 +55,6 @@ export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS'; export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL'; export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST'; -export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS'; export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL'; export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST'; @@ -91,9 +86,10 @@ export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET'; - export const ACCOUNT_REVEAL = 'ACCOUNT_REVEAL'; +export * from './accounts_typed'; + export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); @@ -168,12 +164,12 @@ export function followAccount(id, options = { reblogs: true }) { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); const locked = getState().getIn(['accounts', id, 'locked'], false); - dispatch(followAccountRequest(id, locked)); + dispatch(followAccountRequest({ id, locked })); api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => { - dispatch(followAccountSuccess(response.data, alreadyFollowing)); + dispatch(followAccountSuccess({relationship: response.data, alreadyFollowing})); }).catch(error => { - dispatch(followAccountFail(error, locked)); + dispatch(followAccountFail({ id, error, locked })); }); }; } @@ -183,74 +179,22 @@ export function unfollowAccount(id) { dispatch(unfollowAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { - dispatch(unfollowAccountSuccess(response.data, getState().get('statuses'))); + dispatch(unfollowAccountSuccess({relationship: response.data, statuses: getState().get('statuses')})); }).catch(error => { - dispatch(unfollowAccountFail(error)); + dispatch(unfollowAccountFail({ id, error })); }); }; } -export function followAccountRequest(id, locked) { - return { - type: ACCOUNT_FOLLOW_REQUEST, - id, - locked, - skipLoading: true, - }; -} - -export function followAccountSuccess(relationship, alreadyFollowing) { - return { - type: ACCOUNT_FOLLOW_SUCCESS, - relationship, - alreadyFollowing, - skipLoading: true, - }; -} - -export function followAccountFail(error, locked) { - return { - type: ACCOUNT_FOLLOW_FAIL, - error, - locked, - skipLoading: true, - }; -} - -export function unfollowAccountRequest(id) { - return { - type: ACCOUNT_UNFOLLOW_REQUEST, - id, - skipLoading: true, - }; -} - -export function unfollowAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_UNFOLLOW_SUCCESS, - relationship, - statuses, - skipLoading: true, - }; -} - -export function unfollowAccountFail(error) { - return { - type: ACCOUNT_UNFOLLOW_FAIL, - error, - skipLoading: true, - }; -} - export function blockAccount(id) { return (dispatch, getState) => { dispatch(blockAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/block`).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - dispatch(blockAccountSuccess(response.data, getState().get('statuses'))); + dispatch(blockAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') })); }).catch(error => { - dispatch(blockAccountFail(id, error)); + dispatch(blockAccountFail({ id, error })); }); }; } @@ -260,9 +204,9 @@ export function unblockAccount(id) { dispatch(unblockAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => { - dispatch(unblockAccountSuccess(response.data)); + dispatch(unblockAccountSuccess({ relationship: response.data })); }).catch(error => { - dispatch(unblockAccountFail(id, error)); + dispatch(unblockAccountFail({ id, error })); }); }; } @@ -273,15 +217,6 @@ export function blockAccountRequest(id) { id, }; } - -export function blockAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_BLOCK_SUCCESS, - relationship, - statuses, - }; -} - export function blockAccountFail(error) { return { type: ACCOUNT_BLOCK_FAIL, @@ -296,13 +231,6 @@ export function unblockAccountRequest(id) { }; } -export function unblockAccountSuccess(relationship) { - return { - type: ACCOUNT_UNBLOCK_SUCCESS, - relationship, - }; -} - export function unblockAccountFail(error) { return { type: ACCOUNT_UNBLOCK_FAIL, @@ -317,9 +245,9 @@ export function muteAccount(id, notifications, duration=0) { api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); + dispatch(muteAccountSuccess({ relationship: response.data, statuses: getState().get('statuses') })); }).catch(error => { - dispatch(muteAccountFail(id, error)); + dispatch(muteAccountFail({ id, error })); }); }; } @@ -329,9 +257,9 @@ export function unmuteAccount(id) { dispatch(unmuteAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { - dispatch(unmuteAccountSuccess(response.data)); + dispatch(unmuteAccountSuccess({ relationship: response.data })); }).catch(error => { - dispatch(unmuteAccountFail(id, error)); + dispatch(unmuteAccountFail({ id, error })); }); }; } @@ -343,14 +271,6 @@ export function muteAccountRequest(id) { }; } -export function muteAccountSuccess(relationship, statuses) { - return { - type: ACCOUNT_MUTE_SUCCESS, - relationship, - statuses, - }; -} - export function muteAccountFail(error) { return { type: ACCOUNT_MUTE_FAIL, @@ -365,13 +285,6 @@ export function unmuteAccountRequest(id) { }; } -export function unmuteAccountSuccess(relationship) { - return { - type: ACCOUNT_UNMUTE_SUCCESS, - relationship, - }; -} - export function unmuteAccountFail(error) { return { type: ACCOUNT_UNMUTE_FAIL, @@ -568,7 +481,7 @@ export function fetchRelationships(accountIds) { dispatch(fetchRelationshipsRequest(newAccountIds)); api(getState).get(`/api/v1/accounts/relationships?with_suspended=true&${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { - dispatch(fetchRelationshipsSuccess(response.data)); + dispatch(fetchRelationshipsSuccess({ relationships: response.data })); }).catch(error => { dispatch(fetchRelationshipsFail(error)); }); @@ -583,14 +496,6 @@ export function fetchRelationshipsRequest(ids) { }; } -export function fetchRelationshipsSuccess(relationships) { - return { - type: RELATIONSHIPS_FETCH_SUCCESS, - relationships, - skipLoading: true, - }; -} - export function fetchRelationshipsFail(error) { return { type: RELATIONSHIPS_FETCH_FAIL, @@ -678,7 +583,7 @@ export function authorizeFollowRequest(id) { api(getState) .post(`/api/v1/follow_requests/${id}/authorize`) - .then(() => dispatch(authorizeFollowRequestSuccess(id))) + .then(() => dispatch(authorizeFollowRequestSuccess({ id }))) .catch(error => dispatch(authorizeFollowRequestFail(id, error))); }; } @@ -690,13 +595,6 @@ export function authorizeFollowRequestRequest(id) { }; } -export function authorizeFollowRequestSuccess(id) { - return { - type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS, - id, - }; -} - export function authorizeFollowRequestFail(id, error) { return { type: FOLLOW_REQUEST_AUTHORIZE_FAIL, @@ -712,7 +610,7 @@ export function rejectFollowRequest(id) { api(getState) .post(`/api/v1/follow_requests/${id}/reject`) - .then(() => dispatch(rejectFollowRequestSuccess(id))) + .then(() => dispatch(rejectFollowRequestSuccess({ id }))) .catch(error => dispatch(rejectFollowRequestFail(id, error))); }; } @@ -724,13 +622,6 @@ export function rejectFollowRequestRequest(id) { }; } -export function rejectFollowRequestSuccess(id) { - return { - type: FOLLOW_REQUEST_REJECT_SUCCESS, - id, - }; -} - export function rejectFollowRequestFail(id, error) { return { type: FOLLOW_REQUEST_REJECT_FAIL, @@ -744,7 +635,7 @@ export function pinAccount(id) { dispatch(pinAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/pin`).then(response => { - dispatch(pinAccountSuccess(response.data)); + dispatch(pinAccountSuccess({ relationship: response.data })); }).catch(error => { dispatch(pinAccountFail(error)); }); @@ -756,7 +647,7 @@ export function unpinAccount(id) { dispatch(unpinAccountRequest(id)); api(getState).post(`/api/v1/accounts/${id}/unpin`).then(response => { - dispatch(unpinAccountSuccess(response.data)); + dispatch(unpinAccountSuccess({ relationship: response.data })); }).catch(error => { dispatch(unpinAccountFail(error)); }); @@ -770,13 +661,6 @@ export function pinAccountRequest(id) { }; } -export function pinAccountSuccess(relationship) { - return { - type: ACCOUNT_PIN_SUCCESS, - relationship, - }; -} - export function pinAccountFail(error) { return { type: ACCOUNT_PIN_FAIL, @@ -791,13 +675,6 @@ export function unpinAccountRequest(id) { }; } -export function unpinAccountSuccess(relationship) { - return { - type: ACCOUNT_UNPIN_SUCCESS, - relationship, - }; -} - export function unpinAccountFail(error) { return { type: ACCOUNT_UNPIN_FAIL, @@ -805,11 +682,6 @@ export function unpinAccountFail(error) { }; } -export const revealAccount = id => ({ - type: ACCOUNT_REVEAL, - id, -}); - export function fetchPinnedAccounts() { return (dispatch, getState) => { dispatch(fetchPinnedAccountsRequest()); diff --git a/app/javascript/flavours/glitch/actions/accounts_typed.ts b/app/javascript/flavours/glitch/actions/accounts_typed.ts new file mode 100644 index 0000000000..22aaa48a0d --- /dev/null +++ b/app/javascript/flavours/glitch/actions/accounts_typed.ts @@ -0,0 +1,97 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts'; +import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships'; + +export const revealAccount = createAction<{ + id: string; +}>('accounts/revealAccount'); + +export const importAccounts = createAction<{ accounts: ApiAccountJSON[] }>( + 'accounts/importAccounts', +); + +function actionWithSkipLoadingTrue(args: Args) { + return { + payload: { + ...args, + skipLoading: true, + }, + }; +} + +export const followAccountSuccess = createAction( + 'accounts/followAccount/SUCCESS', + actionWithSkipLoadingTrue<{ + relationship: ApiRelationshipJSON; + alreadyFollowing: boolean; + }>, +); + +export const unfollowAccountSuccess = createAction( + 'accounts/unfollowAccount/SUCCESS', + actionWithSkipLoadingTrue<{ + relationship: ApiRelationshipJSON; + statuses: unknown; + alreadyFollowing?: boolean; + }>, +); + +export const authorizeFollowRequestSuccess = createAction<{ id: string }>( + 'accounts/followRequestAuthorize/SUCCESS', +); + +export const rejectFollowRequestSuccess = createAction<{ id: string }>( + 'accounts/followRequestReject/SUCCESS', +); + +export const followAccountRequest = createAction( + 'accounts/follow/REQUEST', + actionWithSkipLoadingTrue<{ id: string; locked: boolean }>, +); + +export const followAccountFail = createAction( + 'accounts/follow/FAIL', + actionWithSkipLoadingTrue<{ id: string; error: string; locked: boolean }>, +); + +export const unfollowAccountRequest = createAction( + 'accounts/unfollow/REQUEST', + actionWithSkipLoadingTrue<{ id: string }>, +); + +export const unfollowAccountFail = createAction( + 'accounts/unfollow/FAIL', + actionWithSkipLoadingTrue<{ id: string; error: string }>, +); + +export const blockAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; + statuses: unknown; +}>('accounts/block/SUCCESS'); + +export const unblockAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; +}>('accounts/unblock/SUCCESS'); + +export const muteAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; + statuses: unknown; +}>('accounts/mute/SUCCESS'); + +export const unmuteAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; +}>('accounts/unmute/SUCCESS'); + +export const pinAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; +}>('accounts/pin/SUCCESS'); + +export const unpinAccountSuccess = createAction<{ + relationship: ApiRelationshipJSON; +}>('accounts/unpin/SUCCESS'); + +export const fetchRelationshipsSuccess = createAction( + 'relationships/fetch/SUCCESS', + actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>, +); diff --git a/app/javascript/flavours/glitch/actions/domain_blocks.js b/app/javascript/flavours/glitch/actions/domain_blocks.js index d06de20a2d..718002613f 100644 --- a/app/javascript/flavours/glitch/actions/domain_blocks.js +++ b/app/javascript/flavours/glitch/actions/domain_blocks.js @@ -1,11 +1,13 @@ import api, { getLinks } from '../api'; +import { blockDomainSuccess, unblockDomainSuccess } from "./domain_blocks_typed"; + +export * from "./domain_blocks_typed"; + export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; -export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; -export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS'; export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; @@ -24,7 +26,7 @@ export function blockDomain(domain) { const at_domain = '@' + domain; const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); - dispatch(blockDomainSuccess(domain, accounts)); + dispatch(blockDomainSuccess({ domain, accounts })); }).catch(err => { dispatch(blockDomainFail(domain, err)); }); @@ -38,14 +40,6 @@ export function blockDomainRequest(domain) { }; } -export function blockDomainSuccess(domain, accounts) { - return { - type: DOMAIN_BLOCK_SUCCESS, - domain, - accounts, - }; -} - export function blockDomainFail(domain, error) { return { type: DOMAIN_BLOCK_FAIL, @@ -61,7 +55,7 @@ export function unblockDomain(domain) { api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { const at_domain = '@' + domain; const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); - dispatch(unblockDomainSuccess(domain, accounts)); + dispatch(unblockDomainSuccess({ domain, accounts })); }).catch(err => { dispatch(unblockDomainFail(domain, err)); }); @@ -75,14 +69,6 @@ export function unblockDomainRequest(domain) { }; } -export function unblockDomainSuccess(domain, accounts) { - return { - type: DOMAIN_UNBLOCK_SUCCESS, - domain, - accounts, - }; -} - export function unblockDomainFail(domain, error) { return { type: DOMAIN_UNBLOCK_FAIL, diff --git a/app/javascript/flavours/glitch/actions/domain_blocks_typed.ts b/app/javascript/flavours/glitch/actions/domain_blocks_typed.ts new file mode 100644 index 0000000000..c5c4a76ba6 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/domain_blocks_typed.ts @@ -0,0 +1,13 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { Account } from 'flavours/glitch/models/account'; + +export const blockDomainSuccess = createAction<{ + domain: string; + accounts: Account[]; +}>('domain_blocks/block/SUCCESS'); + +export const unblockDomainSuccess = createAction<{ + domain: string; + accounts: Account[]; +}>('domain_blocks/unblock/SUCCESS'); diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index 3d01a96dd8..5fbc9bb5bb 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -1,7 +1,7 @@ -import { normalizeAccount, normalizeStatus, normalizePoll } from './normalizer'; +import { importAccounts } from '../accounts_typed'; + +import { normalizeStatus, normalizePoll } from './normalizer'; -export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; -export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; export const POLLS_IMPORT = 'POLLS_IMPORT'; @@ -13,14 +13,6 @@ function pushUnique(array, object) { } } -export function importAccount(account) { - return { type: ACCOUNT_IMPORT, account }; -} - -export function importAccounts(accounts) { - return { type: ACCOUNTS_IMPORT, accounts }; -} - export function importStatus(status) { return { type: STATUS_IMPORT, status }; } @@ -45,7 +37,7 @@ export function importFetchedAccounts(accounts) { const normalAccounts = []; function processAccount(account) { - pushUnique(normalAccounts, normalizeAccount(account)); + pushUnique(normalAccounts, account); if (account.moved) { processAccount(account.moved); @@ -54,7 +46,7 @@ export function importFetchedAccounts(accounts) { accounts.forEach(processAccount); - return importAccounts(normalAccounts); + return importAccounts({ accounts: normalAccounts }); } export function importFetchedStatus(status) { diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 506e5ee6ad..c2ad0f9908 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -2,7 +2,6 @@ import escapeTextContentForBrowser from 'escape-html'; import emojify from '../../features/emoji/emoji'; import { autoHideCW } from '../../utils/content_warning'; -import { unescapeHTML } from '../../utils/html'; const domParser = new DOMParser(); @@ -17,32 +16,6 @@ export function searchTextFromRawStatus (status) { return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; } -export function normalizeAccount(account) { - account = { ...account }; - - const emojiMap = makeEmojiMap(account.emojis); - const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; - - account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); - account.note_emojified = emojify(account.note, emojiMap); - account.note_plain = unescapeHTML(account.note); - - if (account.fields) { - account.fields = account.fields.map(pair => ({ - ...pair, - name_emojified: emojify(escapeTextContentForBrowser(pair.name), emojiMap), - value_emojified: emojify(pair.value, emojiMap), - value_plain: unescapeHTML(pair.value), - })); - } - - if (account.moved) { - account.moved = account.moved.id; - } - - return account; -} - export function normalizeFilterResult(result) { const normalResult = { ...result }; diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index e63f10359b..6a0bf01b90 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -18,10 +18,12 @@ import { importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; +import { notificationsUpdate } from "./notifications_typed"; import { register as registerPushNotifications } from './push_notifications'; import { saveSettings } from './settings'; -export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; +export * from "./notifications_typed"; + export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; // tracking the notif cleaning request @@ -107,12 +109,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(importFetchedAccount(notification.report.target_account)); } - dispatch({ - type: NOTIFICATIONS_UPDATE, - notification, - usePendingItems: preferPendingItems, - meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, - }); + + dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered})); fetchRelatedRelationships(dispatch, [notification]); } else if (playSound && !filtered) { diff --git a/app/javascript/flavours/glitch/actions/notifications_typed.ts b/app/javascript/flavours/glitch/actions/notifications_typed.ts new file mode 100644 index 0000000000..176362f4b1 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/notifications_typed.ts @@ -0,0 +1,23 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { ApiAccountJSON } from '../api_types/accounts'; +// To be replaced once ApiNotificationJSON type exists +interface FakeApiNotificationJSON { + type: string; + account: ApiAccountJSON; +} + +export const notificationsUpdate = createAction( + 'notifications/update', + ({ + playSound, + ...args + }: { + notification: FakeApiNotificationJSON; + usePendingItems: boolean; + playSound: boolean; + }) => ({ + payload: args, + meta: { sound: playSound ? 'boop' : undefined }, + }), +); diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js index da07142b3b..4a33d7ef87 100644 --- a/app/javascript/flavours/glitch/actions/store.js +++ b/app/javascript/flavours/glitch/actions/store.js @@ -25,6 +25,7 @@ const applyMigrations = (state) => { }); }; + export function hydrateStore(rawState) { return dispatch => { const state = applyMigrations(convertState(rawState)); diff --git a/app/javascript/flavours/glitch/api_types/accounts.ts b/app/javascript/flavours/glitch/api_types/accounts.ts new file mode 100644 index 0000000000..ce55dc604a --- /dev/null +++ b/app/javascript/flavours/glitch/api_types/accounts.ts @@ -0,0 +1,45 @@ +import type { ApiCustomEmojiJSON } from './custom_emoji'; + +export interface ApiAccountFieldJSON { + name: string; + value: string; + verified_at: string | null; +} + +export interface ApiAccountRoleJSON { + color: string; + id: string; + name: string; +} + +// See app/serializers/rest/account_serializer.rb +export interface ApiAccountJSON { + acct: string; + avatar: string; + avatar_static: string; + bot: boolean; + created_at: string; + discoverable: boolean; + display_name: string; + emojis: ApiCustomEmojiJSON[]; + fields: ApiAccountFieldJSON[]; + followers_count: number; + following_count: number; + group: boolean; + header: string; + header_static: string; + id: string; + last_status_at: string; + locked: boolean; + noindex?: boolean; + note: string; + roles?: ApiAccountJSON[]; + statuses_count: number; + uri: string; + url: string; + username: string; + moved?: ApiAccountJSON; + suspended?: boolean; + limited?: boolean; + memorial?: boolean; +} diff --git a/app/javascript/flavours/glitch/api_types/custom_emoji.ts b/app/javascript/flavours/glitch/api_types/custom_emoji.ts new file mode 100644 index 0000000000..05144d6f68 --- /dev/null +++ b/app/javascript/flavours/glitch/api_types/custom_emoji.ts @@ -0,0 +1,8 @@ +// See app/serializers/rest/account_serializer.rb +export interface ApiCustomEmojiJSON { + shortcode: string; + static_url: string; + url: string; + category?: string; + visible_in_picker: boolean; +} diff --git a/app/javascript/flavours/glitch/api_types/relationships.ts b/app/javascript/flavours/glitch/api_types/relationships.ts new file mode 100644 index 0000000000..9f26a0ce9b --- /dev/null +++ b/app/javascript/flavours/glitch/api_types/relationships.ts @@ -0,0 +1,18 @@ +// See app/serializers/rest/relationship_serializer.rb +export interface ApiRelationshipJSON { + blocked_by: boolean; + blocking: boolean; + domain_blocking: boolean; + endorsed: boolean; + followed_by: boolean; + following: boolean; + id: string; + languages: string[] | null; + muting_notifications: boolean; + muting: boolean; + note: string; + notifying: boolean; + requested_by: boolean; + requested: boolean; + showing_reblogs: boolean; +} diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx index 36d8a62b32..109e0daddd 100644 --- a/app/javascript/flavours/glitch/components/account.jsx +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -36,7 +36,7 @@ class Account extends ImmutablePureComponent { static propTypes = { size: PropTypes.number, - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/components/avatar.tsx b/app/javascript/flavours/glitch/components/avatar.tsx index 7ed2d003ef..c45c26981f 100644 --- a/app/javascript/flavours/glitch/components/avatar.tsx +++ b/app/javascript/flavours/glitch/components/avatar.tsx @@ -1,8 +1,9 @@ import classNames from 'classnames'; +import type { Account } from 'flavours/glitch/models/account'; + import { useHovering } from '../hooks/useHovering'; import { autoPlayGif } from '../initial_state'; -import type { Account } from '../types/resources'; interface Props { account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there diff --git a/app/javascript/flavours/glitch/components/avatar_overlay.tsx b/app/javascript/flavours/glitch/components/avatar_overlay.tsx index da9c293856..cddca43d5b 100644 --- a/app/javascript/flavours/glitch/components/avatar_overlay.tsx +++ b/app/javascript/flavours/glitch/components/avatar_overlay.tsx @@ -1,6 +1,7 @@ +import type { Account } from 'flavours/glitch/models/account'; + import { useHovering } from '../hooks/useHovering'; import { autoPlayGif } from '../initial_state'; -import type { Account } from '../types/resources'; interface Props { account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.tsx b/app/javascript/flavours/glitch/components/dismissable_banner.tsx index 68fe1f9a22..817f61df01 100644 --- a/app/javascript/flavours/glitch/components/dismissable_banner.tsx +++ b/app/javascript/flavours/glitch/components/dismissable_banner.tsx @@ -1,9 +1,16 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call, + @typescript-eslint/no-unsafe-return, + @typescript-eslint/no-unsafe-assignment, + @typescript-eslint/no-unsafe-member-access + -- the settings store is not yet typed */ import type { PropsWithChildren } from 'react'; -import { useCallback, useState } from 'react'; +import { useCallback, useState, useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import { changeSetting } from 'flavours/glitch/actions/settings'; import { bannerSettings } from 'flavours/glitch/settings'; +import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { IconButton } from './icon_button'; @@ -19,13 +26,25 @@ export const DismissableBanner: React.FC> = ({ id, children, }) => { - const [visible, setVisible] = useState(!bannerSettings.get(id)); + const dismissed = useAppSelector((state) => + state.settings.getIn(['dismissed_banners', id], false), + ); + const dispatch = useAppDispatch(); + + const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed); const intl = useIntl(); const handleDismiss = useCallback(() => { setVisible(false); bannerSettings.set(id, true); - }, [id]); + dispatch(changeSetting(['dismissed_banners', id], true)); + }, [id, dispatch]); + + useEffect(() => { + if (!visible && !dismissed) { + dispatch(changeSetting(['dismissed_banners', id], true)); + } + }, [id, dispatch, visible, dismissed]); if (!visible) { return null; diff --git a/app/javascript/flavours/glitch/components/display_name.tsx b/app/javascript/flavours/glitch/components/display_name.tsx index 3ad00b5855..82b66b5748 100644 --- a/app/javascript/flavours/glitch/components/display_name.tsx +++ b/app/javascript/flavours/glitch/components/display_name.tsx @@ -4,8 +4,9 @@ import classNames from 'classnames'; import type { List } from 'immutable'; +import type { Account } from 'flavours/glitch/models/account'; + import { autoPlayGif } from '../initial_state'; -import type { Account } from '../types/resources'; import { Skeleton } from './skeleton'; diff --git a/app/javascript/flavours/glitch/components/inline_account.jsx b/app/javascript/flavours/glitch/components/inline_account.jsx index 98e44c05e0..ec3460ea4a 100644 --- a/app/javascript/flavours/glitch/components/inline_account.jsx +++ b/app/javascript/flavours/glitch/components/inline_account.jsx @@ -19,7 +19,7 @@ const makeMapStateToProps = () => { class InlineAccount extends PureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; render () { diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 94754f0f29..141483dd01 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -80,7 +80,7 @@ class Status extends ImmutablePureComponent { containerId: PropTypes.string, id: PropTypes.string, status: ImmutablePropTypes.map, - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, previousId: PropTypes.string, nextInReplyToId: PropTypes.string, rootId: PropTypes.string, diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.jsx b/app/javascript/flavours/glitch/features/account/components/account_note.jsx index bab523acf6..272a4ee312 100644 --- a/app/javascript/flavours/glitch/features/account/components/account_note.jsx +++ b/app/javascript/flavours/glitch/features/account/components/account_note.jsx @@ -49,7 +49,7 @@ class InlineAlert extends PureComponent { class AccountNote extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, value: PropTypes.string, onSave: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx index 720acab43f..1b9e3572db 100644 --- a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx +++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx @@ -15,7 +15,7 @@ const messages = defineMessages({ class FeaturedTags extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, featuredTags: ImmutablePropTypes.list, tagged: PropTypes.string, intl: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx b/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx index 7368ce9758..463499cbf8 100644 --- a/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx +++ b/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx @@ -8,7 +8,7 @@ import { Icon } from 'flavours/glitch/components/icon'; export default class FollowRequestNote extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; render () { diff --git a/app/javascript/flavours/glitch/features/account/components/header.jsx b/app/javascript/flavours/glitch/features/account/components/header.jsx index c4fb2162af..c2b827d9e1 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account/components/header.jsx @@ -83,7 +83,7 @@ const dateFormatOptions = { class Header extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, identity_props: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx index c38c1efa1b..eb2ddbdd80 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx @@ -18,7 +18,7 @@ import MovedNote from './moved_note'; class Header extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx deleted file mode 100644 index d28ad77c55..0000000000 --- a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { revealAccount } from 'flavours/glitch/actions/accounts'; -import { Button } from 'flavours/glitch/components/button'; -import { domain } from 'flavours/glitch/initial_state'; - -const mapDispatchToProps = (dispatch, { accountId }) => ({ - - reveal () { - dispatch(revealAccount(accountId)); - }, - -}); - -class LimitedAccountHint extends PureComponent { - - static propTypes = { - accountId: PropTypes.string.isRequired, - reveal: PropTypes.func, - }; - - render () { - const { reveal } = this.props; - - return ( -
-

- -
- ); - } - -} - -export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.tsx new file mode 100644 index 0000000000..5613811d2f --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.tsx @@ -0,0 +1,35 @@ +import { useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { revealAccount } from 'flavours/glitch/actions/accounts_typed'; +import { Button } from 'flavours/glitch/components/button'; +import { domain } from 'flavours/glitch/initial_state'; +import { useAppDispatch } from 'flavours/glitch/store'; + +export const LimitedAccountHint: React.FC<{ accountId: string }> = ({ + accountId, +}) => { + const dispatch = useAppDispatch(); + const reveal = useCallback(() => { + dispatch(revealAccount({ id: accountId })); + }, [dispatch, accountId]); + + return ( +
+

+ +

+ +
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.jsx b/app/javascript/flavours/glitch/features/account_timeline/index.jsx index d86951fcd3..cf99a1f569 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/index.jsx @@ -20,7 +20,7 @@ import { LoadingIndicator } from '../../components/loading_indicator'; import StatusList from '../../components/status_list'; import Column from '../ui/components/column'; -import LimitedAccountHint from './components/limited_account_hint'; +import { LimitedAccountHint } from './components/limited_account_hint'; import HeaderContainer from './containers/header_container'; const emptyList = ImmutableList(); diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx index ff7d4d03dc..ebaacd7d9a 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -28,7 +28,7 @@ const messages = defineMessages({ class ActionBar extends PureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onLogout: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx index 3efa2edf0a..8ffc9062e5 100644 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx @@ -7,7 +7,7 @@ import { DisplayName } from '../../../components/display_name'; export default class AutosuggestAccount extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; render () { diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index 4ec2b81d11..d815e9db2f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -125,7 +125,7 @@ class ComposeForm extends ImmutablePureComponent { return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (!fulltext.trim().length && !anyMedia)); }; - handleSubmit = (overriddenVisibility = null) => { + handleSubmit = (e, overriddenVisibility = null) => { if (this.props.text !== this.textareaRef.current.value) { // Something changed the text inside the textarea (e.g. browser extensions like Grammarly) // Update the state to match the current text @@ -136,6 +136,10 @@ class ComposeForm extends ImmutablePureComponent { return; } + if (e) { + e.preventDefault(); + } + // Submit unless there are media with missing descriptions if (this.props.mediaDescriptionConfirmation && this.props.media && this.props.media.some(item => !item.get('description'))) { const firstWithoutDescription = this.props.media.find(item => !item.get('description')); @@ -150,10 +154,8 @@ class ComposeForm extends ImmutablePureComponent { // Handles the secondary submit button. handleSecondarySubmit = () => { - const { - sideArm, - } = this.props; - this.handleSubmit(sideArm === 'none' ? null : sideArm); + const { sideArm } = this.props; + this.handleSubmit(null, sideArm === 'none' ? null : sideArm); }; onSuggestionsClearRequested = () => { @@ -342,7 +344,6 @@ class ComposeForm extends ImmutablePureComponent { disabled={!this.canSubmit()} isEditing={isEditing} onSecondarySubmit={this.handleSecondarySubmit} - onSubmit={this.handleSubmit} privacy={privacy} sideArm={sideArm} /> diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx index 85f97a5c91..9f88b0e92a 100644 --- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx @@ -15,7 +15,7 @@ import ActionBar from './action_bar'; export default class NavigationBar extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onLogout: PropTypes.func.isRequired, onClose: PropTypes.func, }; diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.jsx b/app/javascript/flavours/glitch/features/compose/components/publisher.jsx index 5c3a059294..84490ee523 100644 --- a/app/javascript/flavours/glitch/features/compose/components/publisher.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/publisher.jsx @@ -29,16 +29,11 @@ class Publisher extends ImmutablePureComponent { disabled: PropTypes.bool, intl: PropTypes.object.isRequired, onSecondarySubmit: PropTypes.func, - onSubmit: PropTypes.func, privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']), sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']), isEditing: PropTypes.bool, }; - handleSubmit = () => { - this.props.onSubmit(); - }; - render () { const { disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props; @@ -82,9 +77,9 @@ class Publisher extends ImmutablePureComponent {
diff --git a/app/javascript/flavours/glitch/features/compose/components/search.jsx b/app/javascript/flavours/glitch/features/compose/components/search.jsx index 64732068ed..2944be798f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/search.jsx @@ -271,6 +271,7 @@ class Search extends PureComponent { } _calculateOptions (value) { + const { signedIn } = this.context.identity; const trimmedValue = value.trim(); const options = []; @@ -295,7 +296,7 @@ class Search extends PureComponent { const couldBeStatusSearch = searchEnabled; - if (couldBeStatusSearch) { + if (couldBeStatusSearch && signedIn) { options.push({ key: 'status-search', label: {trimmedValue} }} />, action: this.handleStatusSearch }); } @@ -372,7 +373,7 @@ class Search extends PureComponent {

- {searchEnabled ? ( + {searchEnabled && signedIn ? (
{this.defaultOptions.map(({ key, label, action }, i) => (
) : (
- + {searchEnabled ? ( + + ) : ( + + )}
)} diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.jsx b/app/javascript/flavours/glitch/features/directory/components/account_card.jsx index 92fa84201e..27b363bd08 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.jsx +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.jsx @@ -104,7 +104,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ class AccountCard extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, intl: PropTypes.object.isRequired, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx index 54a75dca70..5746af1b39 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx +++ b/app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx @@ -18,7 +18,7 @@ const messages = defineMessages({ class AccountAuthorize extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onAuthorize: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/features/followers/index.jsx b/app/javascript/flavours/glitch/features/followers/index.jsx index f3bcfbf271..6226c96f21 100644 --- a/app/javascript/flavours/glitch/features/followers/index.jsx +++ b/app/javascript/flavours/glitch/features/followers/index.jsx @@ -23,7 +23,7 @@ import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; import ProfileColumnHeader from '../account/components/profile_column_header'; -import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; +import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint'; import HeaderContainer from '../account_timeline/containers/header_container'; import Column from '../ui/components/column'; diff --git a/app/javascript/flavours/glitch/features/following/index.jsx b/app/javascript/flavours/glitch/features/following/index.jsx index 4ff59f6358..6993634302 100644 --- a/app/javascript/flavours/glitch/features/following/index.jsx +++ b/app/javascript/flavours/glitch/features/following/index.jsx @@ -23,7 +23,7 @@ import { LoadingIndicator } from '../../components/loading_indicator'; import ScrollableList from '../../components/scrollable_list'; import AccountContainer from '../../containers/account_container'; import ProfileColumnHeader from '../account/components/profile_column_header'; -import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; +import { LimitedAccountHint } from '../account_timeline/components/limited_account_hint'; import HeaderContainer from '../account_timeline/containers/header_container'; import Column from '../ui/components/column'; diff --git a/app/javascript/flavours/glitch/features/interaction_modal/index.jsx b/app/javascript/flavours/glitch/features/interaction_modal/index.jsx index 7cdd58a12a..1f225e0060 100644 --- a/app/javascript/flavours/glitch/features/interaction_modal/index.jsx +++ b/app/javascript/flavours/glitch/features/interaction_modal/index.jsx @@ -294,9 +294,9 @@ class LoginForm extends React.PureComponent { onFocus={this.handleFocus} onBlur={this.handleBlur} onKeyDown={this.handleKeyDown} - autocomplete='off' - autocapitalize='off' - spellcheck='false' + autoComplete='off' + autoCapitalize='off' + spellCheck='false' /> diff --git a/app/javascript/flavours/glitch/features/list_adder/components/account.jsx b/app/javascript/flavours/glitch/features/list_adder/components/account.jsx index 31a2e96379..94a90726e3 100644 --- a/app/javascript/flavours/glitch/features/list_adder/components/account.jsx +++ b/app/javascript/flavours/glitch/features/list_adder/components/account.jsx @@ -21,7 +21,7 @@ const makeMapStateToProps = () => { class Account extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, }; render () { diff --git a/app/javascript/flavours/glitch/features/list_editor/components/account.jsx b/app/javascript/flavours/glitch/features/list_editor/components/account.jsx index 4618bd1c16..96b5e96df8 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/account.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/components/account.jsx @@ -36,7 +36,7 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({ class Account extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, intl: PropTypes.object.isRequired, onRemove: PropTypes.func.isRequired, onAdd: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx b/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx index 831095200a..e7131829b3 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx @@ -27,7 +27,7 @@ const messages = defineMessages({ class FollowRequest extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onAuthorize: PropTypes.func.isRequired, onReject: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/features/notifications/components/report.jsx b/app/javascript/flavours/glitch/features/notifications/components/report.jsx index decee64db4..0d80582e9f 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/report.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/report.jsx @@ -20,7 +20,7 @@ const messages = defineMessages({ class Report extends ImmutablePureComponent { static propTypes = { - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, report: ImmutablePropTypes.map.isRequired, hidden: PropTypes.bool, intl: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/features/onboarding/index.jsx b/app/javascript/flavours/glitch/features/onboarding/index.jsx index 64cf42efac..3bce573a1d 100644 --- a/app/javascript/flavours/glitch/features/onboarding/index.jsx +++ b/app/javascript/flavours/glitch/features/onboarding/index.jsx @@ -42,7 +42,7 @@ const mapStateToProps = () => { class Onboarding extends ImmutablePureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, multiColumn: PropTypes.bool, ...WithRouterPropTypes, }; diff --git a/app/javascript/flavours/glitch/features/onboarding/share.jsx b/app/javascript/flavours/glitch/features/onboarding/share.jsx index 488096654c..84dc6d8e65 100644 --- a/app/javascript/flavours/glitch/features/onboarding/share.jsx +++ b/app/javascript/flavours/glitch/features/onboarding/share.jsx @@ -145,7 +145,7 @@ class Share extends PureComponent { static propTypes = { onBack: PropTypes.func, - account: ImmutablePropTypes.map, + account: ImmutablePropTypes.record, multiColumn: PropTypes.bool, intl: PropTypes.object, }; diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx index 28dab45dcb..8fd2b98883 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/footer.jsx @@ -162,7 +162,7 @@ class Footer extends ImmutablePureComponent { onClose(); } - history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); + this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); }; render () { diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx index 7a2902bbf0..246dac1808 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx +++ b/app/javascript/flavours/glitch/features/picture_in_picture/components/header.jsx @@ -25,7 +25,7 @@ class Header extends ImmutablePureComponent { static propTypes = { accountId: PropTypes.string.isRequired, statusId: PropTypes.string.isRequired, - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; diff --git a/app/javascript/flavours/glitch/features/report/thanks.jsx b/app/javascript/flavours/glitch/features/report/thanks.jsx index 7782022b77..c928536a8d 100644 --- a/app/javascript/flavours/glitch/features/report/thanks.jsx +++ b/app/javascript/flavours/glitch/features/report/thanks.jsx @@ -20,7 +20,7 @@ class Thanks extends PureComponent { static propTypes = { submitted: PropTypes.bool, onClose: PropTypes.func.isRequired, - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, dispatch: PropTypes.func.isRequired, }; diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.jsx index 59913b4bdb..51a501595b 100644 --- a/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.jsx @@ -59,7 +59,7 @@ class ConfirmationModal extends PureComponent { { onDoNotAsk && (
-
diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx index be7266d746..4230c6dd9a 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx @@ -109,7 +109,7 @@ class FocalPointModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, - account: ImmutablePropTypes.map.isRequired, + account: ImmutablePropTypes.record.isRequired, isUploadingThumbnail: PropTypes.bool, onSave: PropTypes.func.isRequired, onChangeDescription: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx index 40fea635a6..5d8bf2385f 100644 --- a/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/navigation_panel.jsx @@ -66,7 +66,7 @@ class NavigationPanel extends Component { return (
{banner && -