diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 04ac9560ca..f991036add 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # For details, see https://github.com/devcontainers/images/tree/main/src/ruby -FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye +FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye # Install Rails # RUN gem install rails webdrivers diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 4eebb74d13..a2658ea8ba 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -69,7 +69,7 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.3.10 + image: libretranslate/libretranslate:v1.3.11 restart: unless-stopped volumes: - lt-data:/home/libretranslate/.local diff --git a/.eslintrc.js b/.eslintrc.js index 9e965791b0..206faa1c7a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -55,10 +55,7 @@ module.exports = { '\\.(css|scss|json)$', ], 'import/resolver': { - node: { - paths: ['app/javascript'], - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, + typescript: {}, }, }, @@ -84,6 +81,15 @@ module.exports = { { property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substr', message: 'Use .slice instead of .substr.' }, ], + 'no-restricted-syntax': [ + 'error', + { + // eslint-disable-next-line no-restricted-syntax + selector: 'Literal[value=/•/], JSXText[value=/•/]', + // eslint-disable-next-line no-restricted-syntax + message: "Use '·' (middle dot) instead of '•' (bullet)", + }, + ], 'no-self-assign': 'off', 'no-unused-expressions': 'error', 'no-unused-vars': 'off', @@ -101,11 +107,17 @@ module.exports = { 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }], 'react/jsx-boolean-value': 'error', 'react/display-name': 'off', + 'react/jsx-fragments': ['error', 'syntax'], 'react/jsx-equals-spacing': 'error', 'react/jsx-no-bind': 'error', + 'react/jsx-no-useless-fragment': 'error', 'react/jsx-no-target-blank': 'off', + 'react/jsx-tag-spacing': 'error', + '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', // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js @@ -168,11 +180,14 @@ module.exports = { { js: 'never', jsx: 'never', + mjs: 'never', ts: 'never', tsx: 'never', }, ], + 'import/first': 'error', 'import/newline-after-import': 'error', + 'import/no-anonymous-default-export': 'error', 'import/no-extraneous-dependencies': [ 'error', { @@ -187,8 +202,60 @@ module.exports = { 'import/no-amd': 'error', 'import/no-commonjs': 'error', 'import/no-import-module-exports': 'error', + 'import/no-relative-packages': 'error', + 'import/no-self-import': 'error', + 'import/no-useless-path-segments': 'error', 'import/no-webpack-loader-syntax': 'error', + 'import/order': [ + 'error', + { + alphabetize: { order: 'asc' }, + 'newlines-between': 'always', + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + ['index', 'sibling'], + 'object', + ], + pathGroups: [ + // React core packages + { + pattern: '{react,react-dom,react-dom/client,prop-types}', + group: 'builtin', + position: 'after', + }, + // I18n + { + pattern: '{react-intl,intl-messageformat}', + group: 'builtin', + position: 'after', + }, + // Common React utilities + { + pattern: '{classnames,react-helmet,react-router-dom}', + group: 'external', + position: 'before', + }, + // Immutable / Redux / data store + { + pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}', + group: 'external', + position: 'before', + }, + // Internal packages + { + pattern: '{mastodon/**,flavours/glitch-soc/**}', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: [], + }, + ], + 'promise/always-return': 'off', 'promise/catch-or-return': [ 'error', @@ -235,6 +302,7 @@ module.exports = { '.*rc.js', 'ide-helper.js', 'config/webpack/**/*', + 'config/formatjs-formatter.js', ], env: { @@ -258,18 +326,28 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', 'plugin:import/recommended', 'plugin:import/typescript', 'plugin:promise/recommended', - 'plugin:jsdoc/recommended', + 'plugin:jsdoc/recommended-typescript', 'plugin:prettier/recommended', ], + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + rules: { - '@typescript-eslint/no-explicit-any': 'off', + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + + '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/consistent-type-imports': 'error', 'jsdoc/require-jsdoc': 'off', @@ -278,6 +356,9 @@ module.exports = { 'import/no-default-export': 'warn', 'react/prefer-stateless-function': 'warn', 'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }], + 'react/jsx-uses-react': 'off', // not needed with new JSX transform + 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform + 'react/prop-types': 'off', }, }, { diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000000..1ae40d4161 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,114 @@ +{ + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: [ + 'config:base', + ':dependencyDashboard', + ':labels(dependencies)', + ':maintainLockFilesMonthly', // update non-direct dependencies monthly + ':prConcurrentLimit10', // only 10 open PRs at the same time + ], + stabilityDays: 3, // Wait 3 days after the package has been published before upgrading it + // packageRules order is important, they are applied from top to bottom and are merged, + // so for example grouping rules needs to be at the bottom + packageRules: [ + { + // Ignore major version bumps for these node packages + matchManagers: ['npm'], + matchPackageNames: [ + '@rails/ujs', // Needs to match the major Rails version + 'tesseract.js', // Requires code changes + 'react-hotkeys', // Requires code changes + + // Requires Webpacker upgrade or replacement + '@types/webpack', + 'babel-loader', + 'compression-webpack-plugin', + 'css-loader', + 'imports-loader', + 'mini-css-extract-plugin', + 'postcss-loader', + 'sass-loader', + 'terser-webpack-plugin', + 'webpack', + 'webpack-assets-manifest', + 'webpack-bundle-analyzer', + 'webpack-dev-server', + 'webpack-cli', + + // react-router: Requires manual upgrade + 'history', + 'react-router-dom', + ], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Ignore major version bumps for these Ruby packages + matchManagers: ['bundler'], + matchPackageNames: [ + '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 + 'redis', // Requires manual upgrade and sync with Sidekiq version + 'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964 + + // Needs major Rails version bump + 'rack', + 'rails', + 'rails-i18n', + ], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Update Github Actions and Docker images weekly + matchManagers: ['github-actions', 'dockerfile', 'docker-compose'], + extends: ['schedule:weekly'], + }, + { + // Ignore major & minor bumps for the ruby image, this needs to be synced with .ruby-version + matchManagers: ['dockerfile'], + matchPackageNames: ['moritzheiber/ruby-jemalloc'], + matchUpdateTypes: ['minor', 'major'], + enabled: false, + }, + { + // Ignore major bump for the node image, this needs to be synced with .nvmrc + matchManagers: ['dockerfile'], + matchPackageNames: ['node'], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Ignore major postgres bumps in the docker-compose file, as those break dev environments + matchManagers: ['docker-compose'], + matchPackageNames: ['postgres'], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Update devDependencies every week, with one grouped PR + matchDepTypes: 'devDependencies', + matchUpdateTypes: ['patch', 'minor'], + excludePackageNames: [ + 'typescript', // Typescript has many changes in minor versions, needs to be checked every time + ], + groupName: 'devDependencies (non-major)', + extends: ['schedule:weekly'], + }, + { + // Update @types/* packages every week, with one grouped PR + matchPackagePrefixes: '@types/', + matchUpdateTypes: ['patch', 'minor'], + groupName: 'DefinitelyTyped types (non-major)', + extends: ['schedule:weekly'], + addLabels: ['typescript'], + }, + // Add labels depending on package manager + { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] }, + { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] }, + { matchManagers: ['docker-compose', 'dockerfile'], addLabels: ['docker'] }, + { matchManagers: ['github-actions'], addLabels: ['github_actions'] }, + ], +} diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index e282e2ab72..b67c503e95 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -41,8 +41,7 @@ jobs: - name: Check for missing strings in English JSON run: | - yarn build:development - yarn manage:translations en + yarn i18n:extract --throws git diff --exit-code - name: Check locale file normalization diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index e13d227bdb..4d3c2ce5af 100644 --- a/.github/workflows/lint-css.yml +++ b/.github/workflows/lint-css.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -48,4 +49,4 @@ jobs: - run: echo "::add-matcher::.github/stylelint-matcher.json" - name: Stylelint - run: yarn test:lint:sass + run: yarn lint:sass diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 2ddbca7818..56d817123a 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - '.github/workflows/haml-lint-problem-matcher.json' - '.github/workflows/lint-haml.yml' diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 7700e48512..1f0cfd1e70 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -48,7 +49,7 @@ jobs: run: yarn --frozen-lockfile - name: ESLint - run: yarn test:lint:js --max-warnings 0 + run: yarn lint:js --max-warnings 0 - name: Typecheck - run: yarn test:typecheck + run: yarn typecheck diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml index 98f101ad95..8712d8bd80 100644 --- a/.github/workflows/lint-json.yml +++ b/.github/workflows/lint-json.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -40,4 +41,4 @@ jobs: run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.json" + run: yarn lint:json diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml index 6f76dd60c2..d19a0470db 100644 --- a/.github/workflows/lint-md.yml +++ b/.github/workflows/lint-md.yml @@ -3,8 +3,10 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - '.github/workflows/lint-md.yml' + - '.nvmrc' - '.prettier*' - '**/*.md' - '!AUTHORS.md' @@ -14,6 +16,7 @@ on: pull_request: paths: - '.github/workflows/lint-md.yml' + - '.nvmrc' - '.prettier*' - '**/*.md' - '!AUTHORS.md' @@ -32,9 +35,10 @@ jobs: uses: actions/setup-node@v3 with: cache: yarn + node-version-file: '.nvmrc' - name: Install all yarn packages run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.md" + run: yarn lint:md diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml index de54fe9ae5..0395c8639f 100644 --- a/.github/workflows/lint-ruby.yml +++ b/.github/workflows/lint-ruby.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'Gemfile*' - '.rubocop*.yml' diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml index 6f79babcfd..295e9610b3 100644 --- a/.github/workflows/lint-yml.yml +++ b/.github/workflows/lint-yml.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -42,4 +43,4 @@ jobs: run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.{yml,yaml}" + run: yarn lint:yml diff --git a/.github/workflows/rebase-needed.yml b/.github/workflows/rebase-needed.yml index 6a8035210c..131a62a576 100644 --- a/.github/workflows/rebase-needed.yml +++ b/.github/workflows/rebase-needed.yml @@ -4,10 +4,12 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' - 'l10n_main' pull_request_target: branches-ignore: - 'dependabot/**' + - 'renovate/**' - 'l10n_main' types: [synchronize] diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 1c4958550e..3306105f9e 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -44,4 +45,4 @@ jobs: run: yarn --frozen-lockfile - name: Jest testing - run: yarn test:jest --reporters github-actions summary + run: yarn jest --reporters github-actions summary diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index d7e424a8c4..a91fd819a2 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' pull_request: jobs: @@ -23,9 +24,17 @@ jobs: needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + services: postgres: - image: postgres:14-alpine + image: postgres:${{ matrix.postgres}} env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 25bf5ba87f..50266fb8a0 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' pull_request: jobs: @@ -23,9 +24,17 @@ jobs: needs: pre_job if: needs.pre_job.outputs.should_skip != 'true' + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + services: postgres: - image: postgres:14-alpine + image: postgres:${{ matrix.postgres}} env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index f284745ea4..07cb1d41f8 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -4,6 +4,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' pull_request: env: diff --git a/.haml-lint.yml b/.haml-lint.yml index 12ca463422..d1ed30b260 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -4,6 +4,11 @@ exclude: - 'vendor/**/*' - lib/templates/haml/scaffold/_form.html.haml +require: + - ./lib/linter/haml_middle_dot.rb + linters: AltText: enabled: true + MiddleDot: + enabled: true diff --git a/.prettierignore b/.prettierignore index af0411e9cc..27b6d5458a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -61,7 +61,7 @@ docker-compose.override.yml /app/javascript/mastodon/features/emoji/emoji_map.json # Ignore locale files -/app/javascript/mastodon/locales +/app/javascript/mastodon/locales/*.json /config/locales # Ignore vendored CSS reset diff --git a/.rubocop.yml b/.rubocop.yml index ceade6e582..eff89bdaee 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,7 @@ require: - rubocop-rspec - rubocop-performance - rubocop-capybara + - ./lib/linter/rubocop_middle_dot AllCops: TargetRubyVersion: 3.0 # Set to minimum supported version of CI @@ -43,7 +44,7 @@ Layout/LineLength: - !ruby/regexp / \# .*$/ - !ruby/regexp /^\# .*$/ Exclude: - - lib/**/*cli*.rb + - 'lib/mastodon/cli/*.rb' - db/*migrate/**/* - db/seeds/**/* @@ -53,113 +54,52 @@ Lint/UselessAccessModifier: ContextCreatingMethods: - class_methods +## Disable most Metrics/*Length cops +# Reason: those are often triggered and force significant refactors when this happend +# but the team feel they are not really improving the code quality. + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength +Metrics/BlockLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength +Metrics/ClassLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength +Metrics/MethodLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength +Metrics/ModuleLength: + Enabled: false + +## End Disable Metrics/*Length cops + # Reason: Currently disabled in .rubocop_todo.yml # https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize Metrics/AbcSize: Exclude: - - 'lib/**/*cli*.rb' + - 'lib/mastodon/cli/*.rb' - db/*migrate/**/* -# Reason: Some functions cannot be broken up, but others may be refactor candidates -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength -Metrics/BlockLength: - CountAsOne: ['array', 'hash', 'heredoc', 'method_call'] - Exclude: - - 'config/routes.rb' - - 'lib/mastodon/*_cli.rb' - - 'lib/tasks/*.rake' - - 'app/models/concerns/account_associations.rb' - - 'app/models/concerns/account_interactions.rb' - - 'app/models/concerns/ldap_authenticable.rb' - - 'app/models/concerns/omniauthable.rb' - - 'app/models/concerns/pam_authenticable.rb' - - 'app/models/concerns/remotable.rb' - - 'app/services/suspend_account_service.rb' - - 'app/services/unsuspend_account_service.rb' - - 'app/views/accounts/show.rss.ruby' - - 'app/views/tags/show.rss.ruby' - - 'config/environments/development.rb' - - 'config/environments/production.rb' - - 'config/initializers/devise.rb' - - 'config/initializers/doorkeeper.rb' - - 'config/initializers/omniauth.rb' - - 'config/initializers/simple_form.rb' - - 'config/navigation.rb' - - 'config/routes.rb' - - 'config/routes/*.rb' - - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' - - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' - - 'lib/paperclip/gif_transcoder.rb' - # Reason: # https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting Metrics/BlockNesting: Exclude: - - 'lib/mastodon/*_cli.rb' - -# Reason: Some Excluded files would be candidates for refactoring but not currently addressed -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength -Metrics/ClassLength: - CountAsOne: ['array', 'hash', 'heredoc', 'method_call'] - Exclude: - - 'lib/mastodon/*_cli.rb' - - 'app/controllers/admin/accounts_controller.rb' - - 'app/controllers/api/base_controller.rb' - - 'app/controllers/api/v1/admin/accounts_controller.rb' - - 'app/controllers/application_controller.rb' - - 'app/controllers/auth/registrations_controller.rb' - - 'app/controllers/auth/sessions_controller.rb' - - 'app/lib/activitypub/activity.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/activitypub/tag_manager.rb' - - 'app/lib/feed_manager.rb' - - 'app/lib/link_details_extractor.rb' - - 'app/lib/request.rb' - - 'app/lib/text_formatter.rb' - - 'app/lib/user_settings_decorator.rb' - - 'app/mailers/user_mailer.rb' - - 'app/models/account.rb' - - 'app/models/admin/account_action.rb' - - 'app/models/form/account_batch.rb' - - 'app/models/media_attachment.rb' - - 'app/models/status.rb' - - 'app/models/tag.rb' - - 'app/models/user.rb' - - 'app/serializers/activitypub/actor_serializer.rb' - - 'app/serializers/activitypub/note_serializer.rb' - - 'app/serializers/rest/status_serializer.rb' - - 'app/services/account_search_service.rb' - - 'app/services/activitypub/process_account_service.rb' - - 'app/services/activitypub/process_status_update_service.rb' - - 'app/services/backup_service.rb' - - 'app/services/bulk_import_service.rb' - - 'app/services/delete_account_service.rb' - - 'app/services/fan_out_on_write_service.rb' - - 'app/services/fetch_link_card_service.rb' - - 'app/services/import_service.rb' - - 'app/services/notify_service.rb' - - 'app/services/post_status_service.rb' - - 'app/services/update_status_service.rb' - - 'lib/paperclip/color_extractor.rb' + - 'lib/mastodon/cli/*.rb' # Reason: Currently disabled in .rubocop_todo.yml # https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity Metrics/CyclomaticComplexity: Exclude: - - lib/mastodon/*cli*.rb + - lib/mastodon/cli/*.rb - db/*migrate/**/* -# Reason: Currently disabled in .rubocop_todo.yml -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength -Metrics/MethodLength: - CountAsOne: [array, heredoc] - Exclude: - - 'lib/mastodon/*_cli.rb' - # Reason: -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength -Metrics/ModuleLength: - CountAsOne: [array, heredoc] +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists +Metrics/ParameterLists: + CountKeywordArgs: false # Reason: Prevailing style is argument file paths # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath @@ -171,13 +111,12 @@ Rails/FilePath: Rails/HttpStatus: EnforcedStyle: numeric -# Reason: Allowed only in the `tootctl` CLI application code +# Reason: Allowed in `tootctl` CLI code and in boot ENV checker # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsexit Rails/Exit: Exclude: - - 'lib/mastodon/*_cli.rb' - - 'lib/mastodon/cli_helper.rb' - - 'lib/cli.rb' + - 'config/boot.rb' + - 'lib/mastodon/cli/*.rb' # Reason: Some single letter camel case files shouldn't be split # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath @@ -267,3 +206,6 @@ Style/TrailingCommaInArrayLiteral: # https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' + +Style/MiddleDot: + Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index df1b4718ed..c1d580e515 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.50.2. +# using RuboCop version 1.52.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -21,12 +21,6 @@ Layout/ArgumentAlignment: - 'config/initializers/cors.rb' - 'config/initializers/session_store.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. -Layout/ExtraSpacing: - Exclude: - - 'config/initializers/omniauth.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table @@ -39,12 +33,6 @@ Layout/HashAlignment: - 'config/initializers/rack_attack.rb' - 'config/routes.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Width, AllowedPatterns. -Layout/IndentationWidth: - Exclude: - - 'config/initializers/ffmpeg.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. Layout/LeadingCommentSpace: @@ -52,14 +40,6 @@ Layout/LeadingCommentSpace: - 'config/application.rb' - 'config/initializers/omniauth.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. -# SupportedStyles: space, no_space -# SupportedStylesForEmptyBraces: space, no_space -Layout/SpaceBeforeBlockBraces: - Exclude: - - 'config/initializers/paperclip.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_no_space, require_space @@ -69,35 +49,13 @@ Layout/SpaceInLambdaLiteral: - 'config/initializers/content_security_policy.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: space, no_space -Layout/SpaceInsideStringInterpolation: - Exclude: - - 'config/initializers/webauthn.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowInHeredoc. -Layout/TrailingWhitespace: - Exclude: - - 'config/initializers/paperclip.rb' - # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: - - 'spec/controllers/admin/account_moderation_notes_controller_spec.rb' - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' - 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb' - - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' - 'spec/services/activitypub/process_status_update_service_spec.rb' - 'spec/services/post_status_service_spec.rb' - - 'spec/services/suspend_account_service_spec.rb' - - 'spec/services/unsuspend_account_service_spec.rb' - - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -Lint/AmbiguousOperatorPrecedence: - Exclude: - - 'config/initializers/rack_attack.rb' # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: @@ -162,6 +120,7 @@ Lint/UnusedBlockArgument: - 'config/initializers/paperclip.rb' - 'config/initializers/simple_form.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). Lint/UselessAssignment: Exclude: - 'app/services/activitypub/process_status_update_service.rb' @@ -183,6 +142,7 @@ Lint/UselessAssignment: - 'spec/services/resolve_url_service_spec.rb' - 'spec/views/statuses/show.html.haml_spec.rb' +# This cop supports safe autocorrection (--autocorrect). # Configuration parameters: CheckForMethodsWithNoSideEffects. Lint/Void: Exclude: @@ -194,12 +154,6 @@ Metrics/AbcSize: Exclude: - 'app/serializers/initial_state_serializer.rb' -# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. -# AllowedMethods: refine -Metrics/BlockLength: - Exclude: - - 'app/models/concerns/status_safe_reblog_insert.rb' - # Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: Exclude: @@ -209,31 +163,9 @@ Metrics/BlockNesting: Metrics/CyclomaticComplexity: Max: 25 -# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. -Metrics/MethodLength: - Max: 58 - -# Configuration parameters: CountComments, Max, CountAsOne. -Metrics/ModuleLength: - Exclude: - - 'app/controllers/concerns/signature_verification.rb' - - 'app/helpers/application_helper.rb' - - 'app/helpers/jsonld_helper.rb' - - 'app/models/concerns/account_interactions.rb' - - 'app/models/concerns/has_user_settings.rb' - - 'lib/sanitize_ext/sanitize_config.rb' - -# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. -Metrics/ParameterLists: - Exclude: - - 'app/models/concerns/account_interactions.rb' - - 'app/services/activitypub/fetch_remote_account_service.rb' - - 'app/services/activitypub/fetch_remote_actor_service.rb' - - 'app/services/activitypub/fetch_remote_status_service.rb' - # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 28 + Max: 27 Naming/AccessorMethodName: Exclude: @@ -246,6 +178,7 @@ Naming/FileName: Exclude: - 'config/locales/sr-Latn.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: @@ -268,39 +201,9 @@ Naming/VariableNumber: - 'db/migrate/20190820003045_update_statuses_index.rb' - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - 'spec/models/account_spec.rb' - - 'spec/models/concerns/account_interactions_spec.rb' - - 'spec/models/custom_emoji_filter_spec.rb' - 'spec/models/domain_block_spec.rb' - 'spec/models/user_spec.rb' - - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/MapCompact: - Exclude: - - 'app/lib/admin/metrics/dimension.rb' - - 'app/lib/admin/metrics/measure.rb' - - 'app/lib/feed_manager.rb' - - 'app/models/account.rb' - - 'app/models/account_statuses_cleanup_policy.rb' - - 'app/models/account_suggestions/setting_source.rb' - - 'app/models/account_suggestions/source.rb' - - 'app/models/follow_recommendation_filter.rb' - - 'app/models/notification.rb' - - 'app/models/user_role.rb' - - 'app/models/webhook.rb' - - 'app/services/process_mentions_service.rb' - - 'app/validators/existing_username_validator.rb' - - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb' - - 'spec/presenters/status_relationships_presenter_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/StartWith: - Exclude: - - 'app/lib/extractor.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Performance/UnfreezeString: @@ -330,79 +233,6 @@ RSpec/AnyInstance: - 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SkipBlocks, EnforcedStyle. -# SupportedStyles: described_class, explicit -RSpec/DescribedClass: - Exclude: - - 'spec/controllers/concerns/cache_concern_spec.rb' - - 'spec/controllers/concerns/challengable_concern_spec.rb' - - 'spec/lib/entity_cache_spec.rb' - - 'spec/lib/extractor_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - - 'spec/lib/hash_object_spec.rb' - - 'spec/lib/ostatus/tag_manager_spec.rb' - - 'spec/lib/request_spec.rb' - - 'spec/lib/tag_manager_spec.rb' - - 'spec/lib/webfinger_resource_spec.rb' - - 'spec/mailers/notification_mailer_spec.rb' - - 'spec/mailers/user_mailer_spec.rb' - - 'spec/models/account_conversation_spec.rb' - - 'spec/models/account_domain_block_spec.rb' - - 'spec/models/account_migration_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/block_spec.rb' - - 'spec/models/domain_block_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/export_spec.rb' - - 'spec/models/favourite_spec.rb' - - 'spec/models/follow_spec.rb' - - 'spec/models/identity_spec.rb' - - 'spec/models/import_spec.rb' - - 'spec/models/media_attachment_spec.rb' - - 'spec/models/notification_spec.rb' - - 'spec/models/relationship_filter_spec.rb' - - 'spec/models/report_filter_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/models/site_upload_spec.rb' - - 'spec/models/status_pin_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/policies/account_moderation_note_policy_spec.rb' - - 'spec/presenters/account_relationships_presenter_spec.rb' - - 'spec/presenters/status_relationships_presenter_spec.rb' - - 'spec/serializers/activitypub/note_serializer_spec.rb' - - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' - - 'spec/serializers/rest/account_serializer_spec.rb' - - 'spec/services/activitypub/fetch_remote_account_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_actor_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_key_service_spec.rb' - - 'spec/services/after_block_domain_from_account_service_spec.rb' - - 'spec/services/authorize_follow_service_spec.rb' - - 'spec/services/batched_remove_status_service_spec.rb' - - 'spec/services/block_domain_service_spec.rb' - - 'spec/services/block_service_spec.rb' - - 'spec/services/bootstrap_timeline_service_spec.rb' - - 'spec/services/clear_domain_media_service_spec.rb' - - 'spec/services/favourite_service_spec.rb' - - 'spec/services/follow_service_spec.rb' - - 'spec/services/import_service_spec.rb' - - 'spec/services/post_status_service_spec.rb' - - 'spec/services/precompute_feed_service_spec.rb' - - 'spec/services/process_mentions_service_spec.rb' - - 'spec/services/purge_domain_service_spec.rb' - - 'spec/services/reblog_service_spec.rb' - - 'spec/services/reject_follow_service_spec.rb' - - 'spec/services/remove_from_followers_service_spec.rb' - - 'spec/services/remove_status_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - - 'spec/services/unblock_service_spec.rb' - - 'spec/services/unfollow_service_spec.rb' - - 'spec/services/unmute_service_spec.rb' - - 'spec/services/update_account_service_spec.rb' - - 'spec/validators/note_length_validator_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). RSpec/EmptyExampleGroup: Exclude: @@ -442,29 +272,6 @@ RSpec/EmptyExampleGroup: RSpec/ExampleLength: Max: 22 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: method_call, block -RSpec/ExpectChange: - Exclude: - - 'spec/controllers/admin/account_moderation_notes_controller_spec.rb' - - 'spec/controllers/admin/custom_emojis_controller_spec.rb' - - 'spec/controllers/admin/invites_controller_spec.rb' - - 'spec/controllers/admin/report_notes_controller_spec.rb' - - 'spec/controllers/concerns/accountable_concern_spec.rb' - - 'spec/controllers/invites_controller_spec.rb' - - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' - - 'spec/models/admin/account_action_spec.rb' - - 'spec/services/suspend_account_service_spec.rb' - - 'spec/services/unsuspend_account_service_spec.rb' - - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - -RSpec/ExpectInHook: - Exclude: - - 'spec/controllers/api/v1/media_controller_spec.rb' - - 'spec/controllers/settings/applications_controller_spec.rb' - - 'spec/lib/status_filter_spec.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, each, example @@ -509,11 +316,8 @@ RSpec/LetSetup: - 'spec/controllers/admin/statuses_controller_spec.rb' - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb' - 'spec/controllers/api/v1/filters_controller_spec.rb' - 'spec/controllers/api/v1/followed_tags_controller_spec.rb' - - 'spec/controllers/api/v1/tags_controller_spec.rb' - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' @@ -550,7 +354,6 @@ RSpec/LetSetup: - 'spec/services/suspend_account_service_spec.rb' - 'spec/services/unallow_domain_service_spec.rb' - 'spec/services/unsuspend_account_service_spec.rb' - - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' RSpec/MessageChain: @@ -582,32 +385,8 @@ RSpec/MessageSpies: - 'spec/spec_helper.rb' - 'spec/validators/status_length_validator_spec.rb' -RSpec/MissingExampleGroupArgument: - Exclude: - - 'spec/controllers/accounts_controller_spec.rb' - - 'spec/controllers/activitypub/collections_controller_spec.rb' - - 'spec/controllers/admin/statuses_controller_spec.rb' - - 'spec/controllers/admin/users/roles_controller_spec.rb' - - 'spec/controllers/api/v1/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/api/v1/statuses_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/features/log_in_spec.rb' - - 'spec/lib/activitypub/activity/undo_spec.rb' - - 'spec/lib/status_reach_finder_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/trends/statuses_spec.rb' - - 'spec/models/trends/tags_spec.rb' - - 'spec/models/user_role_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/services/fetch_link_card_service_spec.rb' - - 'spec/services/notify_service_spec.rb' - - 'spec/services/process_mentions_service_spec.rb' - RSpec/MultipleExpectations: - Max: 19 + Max: 8 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: @@ -617,53 +396,10 @@ RSpec/MultipleMemoizedHelpers: RSpec/NestedGroups: Max: 6 -# Configuration parameters: AllowedPatterns. -# AllowedPatterns: ^expect_, ^assert_ -RSpec/NoExpectationExample: - Exclude: - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/services/precompute_feed_service_spec.rb' - RSpec/PendingWithoutReason: Exclude: - - 'spec/controllers/statuses_controller_spec.rb' - 'spec/models/account_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers. -# SupportedStyles: inflected, explicit -RSpec/PredicateMatcher: - Exclude: - - 'spec/controllers/api/v1/accounts/notes_controller_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/services/post_status_service_spec.rb' - -RSpec/RepeatedExample: - Exclude: - - 'spec/policies/status_policy_spec.rb' - -RSpec/RepeatedExampleGroupBody: - Exclude: - - 'spec/controllers/statuses_controller_spec.rb' - -RSpec/RepeatedExampleGroupDescription: - Exclude: - - 'spec/controllers/admin/reports/actions_controller_spec.rb' - - 'spec/policies/report_note_policy_spec.rb' - -RSpec/ScatteredSetup: - Exclude: - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/controllers/activitypub/outboxes_controller_spec.rb' - - 'spec/controllers/admin/disputes/appeals_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/services/activitypub/process_account_service_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -RSpec/SharedContext: - Exclude: - - 'spec/services/unsuspend_account_service_spec.rb' - RSpec/StubbedMock: Exclude: - 'spec/controllers/api/base_controller_spec.rb' @@ -678,7 +414,6 @@ RSpec/StubbedMock: RSpec/SubjectDeclaration: Exclude: - 'spec/controllers/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb' - 'spec/models/account_migration_spec.rb' - 'spec/models/account_spec.rb' - 'spec/models/relationship_filter_spec.rb' @@ -704,45 +439,6 @@ RSpec/SubjectStub: - 'spec/services/unallow_domain_service_spec.rb' - 'spec/validators/blacklisted_email_validator_spec.rb' -# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. -RSpec/VerifiedDoubles: - Exclude: - - 'spec/controllers/admin/change_emails_controller_spec.rb' - - 'spec/controllers/admin/confirmations_controller_spec.rb' - - 'spec/controllers/admin/disputes/appeals_controller_spec.rb' - - 'spec/controllers/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/reports_controller_spec.rb' - - 'spec/controllers/api/web/embeds_controller_spec.rb' - - 'spec/controllers/auth/sessions_controller_spec.rb' - - 'spec/controllers/disputes/appeals_controller_spec.rb' - - 'spec/helpers/statuses_helper_spec.rb' - - 'spec/lib/suspicious_sign_in_detector_spec.rb' - - 'spec/models/account/field_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/services/account_search_service_spec.rb' - - 'spec/services/post_status_service_spec.rb' - - 'spec/services/search_service_spec.rb' - - 'spec/validators/blacklisted_email_validator_spec.rb' - - 'spec/validators/disallowed_hashtags_validator_spec.rb' - - 'spec/validators/email_mx_validator_spec.rb' - - 'spec/validators/follow_limit_validator_spec.rb' - - 'spec/validators/note_length_validator_spec.rb' - - 'spec/validators/poll_validator_spec.rb' - - 'spec/validators/status_length_validator_spec.rb' - - 'spec/validators/status_pin_validator_spec.rb' - - 'spec/validators/unique_username_validator_spec.rb' - - 'spec/validators/unreserved_username_validator_spec.rb' - - 'spec/validators/url_validator_spec.rb' - - 'spec/views/statuses/show.html.haml_spec.rb' - - 'spec/workers/activitypub/processing_worker_spec.rb' - - 'spec/workers/admin/domain_purge_worker_spec.rb' - - 'spec/workers/domain_block_worker_spec.rb' - - 'spec/workers/domain_clear_media_worker_spec.rb' - - 'spec/workers/feed_insert_worker_spec.rb' - - 'spec/workers/regeneration_worker_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationController: Exclude: @@ -805,12 +501,6 @@ Rails/DuplicateAssociation: - 'app/serializers/activitypub/collection_serializer.rb' - 'app/serializers/activitypub/note_serializer.rb' -# Configuration parameters: Include. -# Include: app/**/*.rb, config/**/*.rb, lib/**/*.rb -Rails/Exit: - Exclude: - - 'config/boot.rb' - # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasAndBelongsToMany: @@ -859,7 +549,6 @@ Rails/NegateInclude: - 'app/models/concerns/attachmentable.rb' - 'app/models/concerns/remotable.rb' - 'app/models/custom_filter.rb' - - 'app/models/webhook.rb' - 'app/services/activitypub/process_status_update_service.rb' - 'app/services/fetch_link_card_service.rb' - 'app/services/search_service.rb' @@ -945,9 +634,9 @@ Rails/SkipsModelValidations: - '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/cli.rb' - - 'lib/mastodon/accounts_cli.rb' - - 'lib/mastodon/maintenance_cli.rb' + - 'lib/mastodon/cli/accounts.rb' + - 'lib/mastodon/cli/main.rb' + - 'lib/mastodon/cli/maintenance.rb' - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - 'spec/lib/activitypub/activity/follow_spec.rb' - 'spec/services/follow_service_spec.rb' @@ -1029,13 +718,9 @@ Rails/WhereExists: - 'app/validators/vote_validator.rb' - 'app/workers/move_worker.rb' - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' - - 'lib/mastodon/email_domain_blocks_cli.rb' - 'lib/tasks/tests.rake' - - 'spec/controllers/api/v1/accounts/notes_controller_spec.rb' - - 'spec/controllers/api/v1/tags_controller_spec.rb' - 'spec/models/account_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb' - - 'spec/services/post_status_service_spec.rb' - 'spec/services/purge_domain_service_spec.rb' - 'spec/services/unallow_domain_service_spec.rb' @@ -1057,6 +742,7 @@ Style/ClassVars: Exclude: - 'config/initializers/devise.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: Exclude: - 'app/models/form/custom_emoji_batch.rb' @@ -1091,7 +777,6 @@ Style/FormatStringToken: Exclude: - 'app/models/privacy_policy.rb' - 'config/initializers/devise.rb' - - 'lib/mastodon/maintenance_cli.rb' - 'lib/paperclip/color_extractor.rb' # This cop supports unsafe autocorrection (--autocorrect-all). @@ -1501,11 +1186,6 @@ Style/GlobalStdStream: - 'config/environments/development.rb' - 'config/environments/production.rb' -# Configuration parameters: AllowedVariables. -Style/GlobalVars: - Exclude: - - 'config/initializers/statsd.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: @@ -1536,9 +1216,9 @@ Style/GuardClause: - '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/mastodon/accounts_cli.rb' - - 'lib/mastodon/maintenance_cli.rb' - - 'lib/mastodon/media_cli.rb' + - 'lib/mastodon/cli/accounts.rb' + - 'lib/mastodon/cli/maintenance.rb' + - 'lib/mastodon/cli/media.rb' - 'lib/paperclip/attachment_extensions.rb' - 'lib/tasks/repo.rake' @@ -1639,7 +1319,6 @@ Style/RedundantConstantBase: Exclude: - 'config/environments/production.rb' - 'config/initializers/sidekiq.rb' - - 'config/initializers/statsd.rb' - 'config/locales/sr-Latn.rb' - 'config/locales/sr.rb' @@ -1653,52 +1332,6 @@ Style/RedundantFetchBlock: - 'config/initializers/paperclip.rb' - 'config/puma.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: - Exclude: - - 'app/lib/link_details_extractor.rb' - - 'app/lib/tag_manager.rb' - - 'app/models/domain_allow.rb' - - 'app/models/domain_block.rb' - - 'app/services/fetch_oembed_service.rb' - - 'config/initializers/rack_attack.rb' - - 'lib/tasks/emojis.rake' - - 'lib/tasks/mastodon.rake' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpEscape: - Exclude: - - 'app/lib/webfinger_resource.rb' - - 'app/models/account.rb' - - 'app/models/tag.rb' - - 'app/services/fetch_link_card_service.rb' - - 'config/initializers/twitter_regex.rb' - - 'lib/paperclip/color_extractor.rb' - - 'lib/tasks/mastodon.rake' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowInnerSlashes. -# SupportedStyles: slashes, percent_r, mixed -Style/RegexpLiteral: - Exclude: - - 'app/lib/link_details_extractor.rb' - - 'app/lib/plain_text_formatter.rb' - - 'app/lib/tag_manager.rb' - - 'app/lib/text_formatter.rb' - - 'app/models/account.rb' - - 'app/models/domain_allow.rb' - - 'app/models/domain_block.rb' - - 'app/models/site_upload.rb' - - 'app/models/tag.rb' - - 'app/services/backup_service.rb' - - 'app/services/fetch_oembed_service.rb' - - 'app/services/search_service.rb' - - 'config/initializers/rack_attack.rb' - - 'config/initializers/twitter_regex.rb' - - 'config/routes.rb' - - 'lib/mastodon/premailer_webpack_strategy.rb' - - 'lib/tasks/mastodon.rake' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! diff --git a/Dockerfile b/Dockerfile index 3773cdba6f..8a9fd15f04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -79,7 +79,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" -# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use +# 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 && \ diff --git a/Gemfile b/Gemfile index 567d4e5bfb..7a0fbdc82d 100644 --- a/Gemfile +++ b/Gemfile @@ -3,9 +3,7 @@ source 'https://rubygems.org' ruby '>= 3.0.0' -gem 'pkg-config', '~> 1.5' - -gem 'puma', '~> 6.2' +gem 'puma', '~> 6.3' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' @@ -17,10 +15,10 @@ gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.120', require: false +gem 'aws-sdk-s3', '~> 1.123', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b' +gem 'kt-paperclip', '~> 7.2' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' @@ -59,8 +57,7 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' -gem 'nokogiri', '~> 1.14' -gem 'nsa', '~> 0.2' +gem 'nokogiri', '~> 1.15' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' @@ -75,7 +72,7 @@ gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-s gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'rqrcode', '~> 2.1' +gem 'rqrcode', '~> 2.2' gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' @@ -99,56 +96,87 @@ gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -group :development, :test do - gem 'fabrication', '~> 2.30' - gem 'fuubar', '~> 2.5' - gem 'i18n-tasks', '~> 1.0', require: false - gem 'rspec-rails', '~> 6.0' - gem 'rspec_chunked', '~> 0.6' - - gem 'rubocop-capybara', require: false - gem 'rubocop-performance', require: false - gem 'rubocop-rails', require: false - gem 'rubocop-rspec', require: false - gem 'rubocop', require: false -end - -group :production, :test do - gem 'private_address_check', '~> 0.5' -end +gem 'private_address_check', '~> 0.5' group :test do - gem 'capybara', '~> 3.39' - gem 'climate_control' - gem 'faker', '~> 3.2' - gem 'json-schema', '~> 4.0' - gem 'rack-test', '~> 2.1' - gem 'rails-controller-testing', '~> 1.0' - gem 'rspec_junit_formatter', '~> 0.6' + # RSpec runner for rails + gem 'rspec-rails', '~> 6.0' + + # Used to split testing into chunks in CI + gem 'rspec_chunked', '~> 0.6' + + # RSpec progress bar formatter + gem 'fuubar', '~> 2.5' + + # Extra RSpec extenion methods and helpers for sidekiq gem 'rspec-sidekiq', '~> 3.1' + + # Browser integration testing + gem 'capybara', '~> 3.39' + + # Used to mock environment variables + gem 'climate_control', '~> 0.2' + + # Generating fake data for specs + gem 'faker', '~> 3.2' + + # Generate test objects for specs + gem 'fabrication', '~> 2.30' + + # Add back helpers functions removed in Rails 5.1 + gem 'rails-controller-testing', '~> 1.0' + + # Validate schemas in specs + gem 'json-schema', '~> 4.0' + + # Test harness fo rack components + gem 'rack-test', '~> 2.1' + + # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false + + # Stub web requests for specs gem 'webmock', '~> 3.18' end group :development do + # Code linting CLI and plugins + gem 'rubocop', require: false + gem 'rubocop-capybara', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + + # Annotates modules with schema gem 'annotate', '~> 3.2' + + # Enhanced error message pages for development gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 1.0' + + # Preview mail in the browser gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' - gem 'memory_profiler' + + # Security analysis CLI tools gem 'brakeman', '~> 5.4', require: false gem 'bundler-audit', '~> 0.9', require: false + + # Linter CLI for HAML files gem 'haml_lint', require: false + # Deployment automation gem 'capistrano', '~> 3.17' gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-yarn', '~> 2.0' - gem 'stackprof' + # Validate missing i18n keys + gem 'i18n-tasks', '~> 1.0', require: false - gem 'foreman' + # Profiling tools + gem 'memory_profiler', require: false + gem 'stackprof', require: false end group :production do @@ -159,8 +187,9 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'hcaptcha', '~> 7.1' gem 'cocoon', '~> 1.2' gem 'net-http', '~> 0.3.2' gem 'rubyzip', '~> 2.3' + +gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index 167f20fada..b2d75e9d4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,18 +7,6 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) -GIT - remote: https://github.com/kreeti/kt-paperclip.git - revision: 11abf222dc31bff71160a1d138b445214f434b2b - ref: 11abf222dc31bff71160a1d138b445214f434b2b - specs: - kt-paperclip (7.1.1) - activemodel (>= 4.2.0) - activesupport (>= 4.2.0) - marcel (~> 1.0.1) - mime-types - terrapin (~> 0.6.0) - GIT remote: https://github.com/mastodon/rails-settings-cached.git revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab @@ -30,40 +18,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + actioncable (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailbox (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (>= 2.7.1) - actionmailer (6.1.7.3) - actionpack (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionmailer (6.1.7.4) + actionpack (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.3) - actionview (= 6.1.7.3) - activesupport (= 6.1.7.3) + actionpack (6.1.7.4) + actionview (= 6.1.7.4) + activesupport (= 6.1.7.4) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.3) - actionpack (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + actiontext (6.1.7.4) + actionpack (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) nokogiri (>= 1.8.5) - actionview (6.1.7.3) - activesupport (= 6.1.7.3) + actionview (6.1.7.4) + activesupport (= 6.1.7.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -73,22 +61,22 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (6.1.7.3) - activesupport (= 6.1.7.3) + activejob (6.1.7.4) + activesupport (= 6.1.7.4) globalid (>= 0.3.6) - activemodel (6.1.7.3) - activesupport (= 6.1.7.3) - activerecord (6.1.7.3) - activemodel (= 6.1.7.3) - activesupport (= 6.1.7.3) - activestorage (6.1.7.3) - actionpack (= 6.1.7.3) - activejob (= 6.1.7.3) - activerecord (= 6.1.7.3) - activesupport (= 6.1.7.3) + activemodel (6.1.7.4) + activesupport (= 6.1.7.4) + activerecord (6.1.7.4) + activemodel (= 6.1.7.4) + activesupport (= 6.1.7.4) + activestorage (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activesupport (= 6.1.7.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.3) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -109,26 +97,26 @@ GEM attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.752.0) - aws-sdk-core (3.171.0) + aws-partitions (1.780.0) + aws-sdk-core (3.175.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.63.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.67.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.121.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.126.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) bcrypt (3.1.18) - better_errors (2.9.1) - coderay (>= 1.0.0) + better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) + rouge (>= 1.0.0) better_html (2.0.1) actionview (>= 6.0) activesupport (>= 6.0) @@ -151,7 +139,7 @@ GEM bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - capistrano (3.17.2) + capistrano (3.17.3) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) @@ -166,7 +154,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.39.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) @@ -186,10 +174,9 @@ GEM chunky_png (1.4.0) climate_control (0.2.0) cocoon (1.2.15) - coderay (1.1.3) color_diff (0.1) concurrent-ruby (1.2.2) - connection_pool (2.4.0) + connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -241,7 +228,7 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.99.0) + excon (0.100.0) fabrication (2.30.0) faker (3.2.0) i18n (>= 1.8.11, < 2) @@ -269,7 +256,7 @@ GEM faraday-rack (1.0.0) faraday-retry (1.0.3) fast_blank (1.0.1) - fastimage (2.2.6) + fastimage (2.2.7) ffi (1.15.5) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -286,7 +273,6 @@ GEM fog-core (>= 1.45, <= 2.1.0) fog-json (>= 1.0) ipaddress (>= 0.8) - foreman (0.87.2) formatador (0.3.0) fugit (1.8.1) et-orbi (~> 1, >= 1.2.7) @@ -332,7 +318,7 @@ GEM httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -368,7 +354,7 @@ GEM json-schema (4.0.0) addressable (>= 2.8) jsonapi-renderer (0.2.2) - jwt (2.7.0) + jwt (2.7.1) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -381,6 +367,12 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + kt-paperclip (7.2.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + marcel (~> 1.0.1) + mime-types + terrapin (~> 0.6.0) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) @@ -399,9 +391,9 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.20.0) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) + nokogiri (>= 1.12.0) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -419,14 +411,14 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2023.0218.1) mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.18.0) - msgpack (1.7.0) + mini_portile2 (2.8.2) + minitest (5.18.1) + msgpack (1.7.1) multi_json (1.15.0) multipart-post (2.3.0) net-http (0.3.2) uri - net-imap (0.3.4) + net-imap (0.3.6) date net-protocol net-ldap (0.18.0) @@ -440,15 +432,10 @@ GEM net-protocol net-ssh (7.1.0) nio4r (2.5.9) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + nokogiri (1.15.2) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - nsa (0.2.8) - activesupport (>= 4.2, < 7) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.14.3) + oj (3.15.0) omniauth (1.9.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -482,15 +469,15 @@ GEM orm_adapter (0.5.0) ox (2.14.16) parallel (1.23.0) - parser (3.2.2.1) + parser (3.2.2.3) ast (~> 2.4.1) + racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) pg (1.5.3) pghero (3.3.3) activerecord (>= 6) - pkg-config (1.5.1) posix-spawn (0.3.15) premailer (1.21.0) addressable @@ -502,12 +489,12 @@ GEM premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) public_suffix (5.0.1) - puma (6.2.2) + puma (6.3.0) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.2) + racc (1.7.1) rack (2.2.7) rack-attack (6.6.1) rack (>= 1.0, < 3) @@ -523,20 +510,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.3) - actioncable (= 6.1.7.3) - actionmailbox (= 6.1.7.3) - actionmailer (= 6.1.7.3) - actionpack (= 6.1.7.3) - actiontext (= 6.1.7.3) - actionview (= 6.1.7.3) - activejob (= 6.1.7.3) - activemodel (= 6.1.7.3) - activerecord (= 6.1.7.3) - activestorage (= 6.1.7.3) - activesupport (= 6.1.7.3) + rails (6.1.7.4) + actioncable (= 6.1.7.4) + actionmailbox (= 6.1.7.4) + actionmailer (= 6.1.7.4) + actionpack (= 6.1.7.4) + actiontext (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activemodel (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) bundler (>= 1.15.0) - railties (= 6.1.7.3) + railties (= 6.1.7.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -545,30 +532,31 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) - railties (6.1.7.3) - actionpack (= 6.1.7.3) - activesupport (= 6.1.7.3) + railties (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) method_source rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) rake (13.0.6) - rdf (3.2.10) + rdf (3.2.11) link_header (~> 0.0, >= 0.0.8) - rdf-normalize (0.5.1) + rdf-normalize (0.6.0) rdf (~> 3.2) redcarpet (3.6.0) redis (4.8.1) - redis-namespace (1.10.0) + redis-namespace (1.11.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.8.0) + regexp_parser (2.8.1) request_store (1.5.1) rack (>= 1.4) responders (3.1.0) @@ -576,8 +564,9 @@ GEM railties (>= 5.2) rexml (3.2.5) rotp (6.2.2) + rouge (4.1.2) rpam2 (4.0.2) - rqrcode (2.1.2) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) @@ -589,52 +578,53 @@ GEM rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.1) + rspec-rails (6.0.3) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) - rspec-core (~> 3.11) - rspec-expectations (~> 3.11) - rspec-mocks (~> 3.11) - rspec-support (~> 3.11) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.12.0) rspec_chunked (0.6) - rspec_junit_formatter (0.6.0) - rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.50.2) + rubocop (1.52.1) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.28.0) + rubocop-ast (1.29.0) parser (>= 3.2.1.0) rubocop-capybara (2.18.0) rubocop (~> 1.41) - rubocop-performance (1.17.1) + rubocop-factory_bot (2.23.1) + rubocop (~> 1.33) + rubocop-performance (1.18.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) rubocop-rails (2.19.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.19.0) + rubocop-rspec (2.22.0) rubocop (~> 1.33) rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) - ruby-saml (1.13.0) - nokogiri (>= 1.10.5) + ruby-saml (1.15.0) + nokogiri (>= 1.13.10) rexml ruby2_keywords (0.0.5) rubyzip (2.3.2) - rufus-scheduler (3.8.2) + rufus-scheduler (3.9.1) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) @@ -645,13 +635,13 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) semantic_range (3.0.0) - sidekiq (6.5.8) + sidekiq (6.5.9) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (5.0.2) + sidekiq-scheduler (5.0.3) rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0) @@ -684,7 +674,6 @@ GEM net-scp (>= 1.1.2) net-ssh (>= 2.8.0) stackprof (0.2.25) - statsd-ruby (1.5.0) stoplight (3.0.1) redlock (~> 1.0) strong_migrations (0.8.0) @@ -694,13 +683,13 @@ GEM attr_required (>= 0.0.5) httpclient (>= 2.4) sysexits (1.2.0) - temple (0.10.0) + temple (0.10.2) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - thor (1.2.1) - tilt (2.1.0) + thor (1.2.2) + tilt (2.2.0) timeout (0.3.2) tpm-key_attestation (0.12.0) bindata (~> 2.4) @@ -727,7 +716,7 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - uri (0.12.1) + uri (0.12.2) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -764,7 +753,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.7) + zeitwerk (2.6.8) PLATFORMS ruby @@ -773,7 +762,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.120) + aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) @@ -788,7 +777,7 @@ DEPENDENCIES capybara (~> 3.39) charlock_holmes (~> 0.7.7) chewy (~> 7.3) - climate_control + climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby @@ -806,7 +795,6 @@ DEPENDENCIES fastimage fog-core (<= 2.4.0) fog-openstack (~> 0.3) - foreman fuubar (~> 2.5) haml-rails (~> 2.0) haml_lint @@ -822,7 +810,7 @@ DEPENDENCIES json-ld-preloaded (~> 3.2) json-schema (~> 4.0) kaminari (~> 1.2) - kt-paperclip (~> 7.1)! + kt-paperclip (~> 7.2) letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) @@ -833,8 +821,7 @@ DEPENDENCIES mime-types (~> 3.4.1) net-http (~> 0.3.2) net-ldap (~> 0.18) - nokogiri (~> 1.14) - nsa (~> 0.2) + nokogiri (~> 1.15) oj (~> 3.14) omniauth (~> 1.9) omniauth-cas (~> 2.0) @@ -845,12 +832,11 @@ DEPENDENCIES parslet pg (~> 1.5) pghero - pkg-config (~> 1.5) posix-spawn premailer-rails private_address_check (~> 0.5) public_suffix (~> 5.0) - puma (~> 6.2) + puma (~> 6.3) pundit (~> 2.3) rack (~> 2.2.7) rack-attack (~> 6.6) @@ -864,11 +850,10 @@ DEPENDENCIES redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) - rqrcode (~> 2.1) + rqrcode (~> 2.2) rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) rspec_chunked (~> 0.6) - rspec_junit_formatter (~> 0.6) rubocop rubocop-capybara rubocop-performance @@ -901,7 +886,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.0.5p211 + ruby 3.2.2p53 BUNDLED WITH - 2.4.6 + 2.4.13 diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index e38e14a106..abde8e92f1 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -2,8 +2,37 @@ class AccountsIndex < Chewy::Index settings index: { refresh_interval: '30s' }, analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { - content: { + natural: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + + verbatim: { tokenizer: 'whitespace', filter: %w(lowercase asciifolding cjk_width), }, @@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index index_scope ::Account.searchable.includes(:account_stat) root date_detection: false do - field :id, type: 'long' - - field :display_name, type: 'text', analyzer: 'content' do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :following_count, type: 'long', value: ->(account) { account.following_count } - field :followers_count, type: 'long', value: ->(account) { account.followers_count } - field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } + field(:id, type: 'long') + 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(: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', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' } end end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 099512248f..3a6df662ea 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -14,15 +14,5 @@ module Admin @pending_tags_count = Tag.pending_review.count @pending_appeals_count = Appeal.pending.count end - - private - - def redis_info - @redis_info ||= if redis.is_a?(Redis::Namespace) - redis.redis.info - else - redis.info - end - end end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 081550b762..b9691c5a3a 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -31,31 +31,41 @@ module Admin @domain_block = DomainBlock.new(resource_params) existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil + # Disallow accidentally downgrading a domain block if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe @domain_block.errors.delete(:domain) - render :new - else - if existing_domain_block.present? - @domain_block = existing_domain_block - @domain_block.update(resource_params) - end + return render :new + end - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id) - log_action :create, @domain_block - redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') - else - render :new - end + # Allow transparently upgrading a domain block + if existing_domain_block.present? + @domain_block = existing_domain_block + @domain_block.assign_attributes(resource_params) + end + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + else + render :new end end def update authorize :domain_block, :update? - if @domain_block.update(update_params) + @domain_block.assign_attributes(update_params) + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') @@ -92,5 +102,9 @@ module Admin def action_from_button 'save' if params[:save] end + + def requires_confirmation? + @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm] + end end end diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index 1ed3fd18ab..f1aad7c4b5 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -28,6 +28,7 @@ module Admin authorize :webhook, :create? @webhook = Webhook.new(resource_params) + @webhook.current_account = current_account if @webhook.save redirect_to admin_webhook_path(@webhook) @@ -39,10 +40,12 @@ module Admin def update authorize @webhook, :update? + @webhook.current_account = current_account + if @webhook.update(resource_params) redirect_to admin_webhook_path(@webhook) else - render :show + render :edit end end @@ -71,7 +74,7 @@ module Admin end def resource_params - params.require(:webhook).permit(:url, events: []) + params.require(:webhook).permit(:url, :template, events: []) end end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 8af4242ba3..ddb94d5ca4 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -90,7 +90,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason) + params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone) end def check_enabled_registrations diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb index 9ef1b3be71..7b192b979f 100644 --- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -58,7 +58,7 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController end def set_canonical_email_blocks_from_test - @canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email]) + @canonical_email_blocks = CanonicalEmailBlock.matching_email(params.require(:email)) end def set_canonical_email_block diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb index 61e1d481c7..dd54d67106 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -29,7 +29,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController def create authorize :domain_allow, :create? - @domain_allow = DomainAllow.find_by(resource_params) + @domain_allow = DomainAllow.find_by(domain: resource_params[:domain]) if @domain_allow.nil? @domain_allow = DomainAllow.create!(resource_params) diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 9034e8a2f4..b3ca2f7903 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController def index @conversations = paginated_conversations - render json: @conversations, each_serializer: REST::ConversationSerializer + render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id) end def read @@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController render json: @conversation, serializer: REST::ConversationSerializer end + def unread + @conversation.update!(unread: true) + render json: @conversation, serializer: REST::ConversationSerializer + end + def destroy @conversation.destroy! render_empty @@ -32,6 +37,19 @@ class Api::V1::ConversationsController < Api::BaseController def paginated_conversations AccountConversation.where(account: current_account) + .includes( + account: :account_stat, + last_status: [ + :media_attachments, + :preview_cards, + :status_stat, + :tags, + { + active_mentions: [account: :account_stat], + account: :account_stat, + }, + ] + ) .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c0585e8599..1109435507 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -21,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController def accounts_scope Account.discoverable.tap do |scope| - scope.merge!(Account.local) if truthy_param?(:local) - scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active' - scope.merge!(Account.order(id: :desc)) if params[:order] == 'new' - scope.merge!(Account.not_excluded_by_account(current_account)) if current_account - scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local) + scope.merge!(account_order_scope) + scope.merge!(local_account_scope) if local_accounts? + scope.merge!(account_exclusion_scope) if current_account + scope.merge!(account_domain_block_scope) if current_account && !local_accounts? end end + + def local_accounts? + truthy_param?(:local) + end + + def account_order_scope + case params[:order] + when 'new' + Account.order(id: :desc) + when 'active', nil + Account.by_recent_status + end + end + + def local_account_scope + Account.local + end + + def account_exclusion_scope + Account.not_excluded_by_account(current_account) + end + + def account_domain_block_scope + Account.not_domain_blocked_by_account(current_account) + end end diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb index 32fb8e39fa..16e91b4497 100644 --- a/app/controllers/api/v1/emails/confirmations_controller.rb +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true class Api::V1::Emails::ConfirmationsController < Api::BaseController - before_action -> { doorkeeper_authorize! :write, :'write:accounts' } - before_action :require_user_owned_by_application! - before_action :require_user_not_confirmed! + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :check + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check + before_action :require_user_owned_by_application!, except: :check + before_action :require_user_not_confirmed!, except: :check + before_action :require_authenticated_user!, only: :check def create current_user.update!(email: params[:email]) if params.key?(:email) @@ -12,6 +14,10 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController render_empty end + def check + render json: current_user.confirmed? + end + private def require_user_owned_by_application! diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb index edb42a94ea..516046f009 100644 --- a/app/controllers/api/v1/featured_tags_controller.rb +++ b/app/controllers/api/v1/featured_tags_controller.rb @@ -13,7 +13,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def create - featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name]) + featured_tag = CreateFeaturedTagService.new.call(current_account, params.require(:name)) render json: featured_tag, serializer: REST::FeaturedTagSerializer end @@ -31,8 +31,4 @@ class Api::V1::FeaturedTagsController < Api::BaseController def set_featured_tags @featured_tags = current_account.featured_tags.order(statuses_count: :desc) end - - def featured_tag_params - params.permit(:name) - end end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 843ca2ec2b..4bbbed2673 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy) + params.permit(:title, :replies_policy, :exclusive) end end diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index dff2425d06..2913472b04 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -8,11 +8,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController def show cache_if_unauthenticated! - render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer + render json: status_edits, each_serializer: REST::StatusEditSerializer end private + def status_edits + @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] + end + def set_status @status = Status.find(params[:status_id]) authorize @status, :show? diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 1be15a5a43..e3769437b7 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -2,6 +2,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController include Authorization + include Redisable + include Lockable before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action :require_user! @@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController override_rate_limit_headers :create, family: :statuses def create - @status = ReblogService.new.call(current_account, @reblog, reblog_params) + with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do + @status = ReblogService.new.call(current_account, @reblog, reblog_params) + end render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v2/admin/accounts_controller.rb b/app/controllers/api/v2/admin/accounts_controller.rb index 0c451f778c..65cf0c4db4 100644 --- a/app/controllers/api/v2/admin/accounts_controller.rb +++ b/app/controllers/api/v2/admin/accounts_controller.rb @@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController private + def next_path + api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + def filtered_accounts AccountFilter.new(translated_filter_params).results end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 620fb621dc..3283c5f362 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -57,9 +57,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController def captcha_user_bypass? return true if @confirmation_user.nil? || @confirmation_user.confirmed? - - invite = Invite.find(@confirmation_user.invite_id) if @confirmation_user.invite_id.present? - invite.present? && !invite.max_uses.nil? end def set_pack @@ -91,8 +88,10 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController def after_confirmation_path_for(_resource_name, user) if user.created_by_application && truthy_param?(:redirect_to_app) user.created_by_application.confirmation_redirect_uri + elsif user_signed_in? + web_url('start') else - super + new_user_session_path end end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index a49d23a9f5..a9d92b6e2b 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -132,7 +132,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_sessions - @sessions = current_user.session_activations + @sessions = current_user.session_activations.order(updated_at: :desc) end def set_strikes diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 3ee35d141c..8edca4d01b 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -45,6 +45,6 @@ class Auth::SetupController < ApplicationController end def set_pack - use_pack 'auth' + use_pack 'sign_up' end end diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 5891da6f6d..205df48d44 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -11,15 +11,15 @@ class BackupsController < ApplicationController def download case Paperclip::Attachment.default_options[:storage] when :s3 - redirect_to @backup.dump.expiring_url(10) + redirect_to @backup.dump.expiring_url(10), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(Time.now.utc + 10) + redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true else - redirect_to full_asset_url(@backup.dump.url) + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end when :filesystem - redirect_to full_asset_url(@backup.dump.url) + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end end diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb index 538c1ffb14..576304d1ca 100644 --- a/app/controllers/concerns/captcha_concern.rb +++ b/app/controllers/concerns/captcha_concern.rb @@ -2,6 +2,7 @@ module CaptchaConcern extend ActiveSupport::Concern + include Hcaptcha::Adapters::ViewMethods included do @@ -35,18 +36,22 @@ module CaptchaConcern flash.delete(:hcaptcha_error) yield message end + false end end def extend_csp_for_captcha! policy = request.content_security_policy + return unless captcha_required? && policy.present? %w(script_src frame_src style_src connect_src).each do |directive| values = policy.send(directive) + values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:') values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:') + policy.send(directive, *values) end end diff --git a/app/controllers/concerns/theming_concern.rb b/app/controllers/concerns/theming_concern.rb index f993a81d72..82a53dbf51 100644 --- a/app/controllers/concerns/theming_concern.rb +++ b/app/controllers/concerns/theming_concern.rb @@ -75,7 +75,7 @@ module ThemingConcern end fallbacks.each do |fallback| - return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback) + return resolve_pack(Themes.instance.flavour(fallback), pack_name, skin) if Themes.instance.flavour(fallback) end nil diff --git a/app/controllers/mail_subscriptions_controller.rb b/app/controllers/mail_subscriptions_controller.rb new file mode 100644 index 0000000000..b071a80605 --- /dev/null +++ b/app/controllers/mail_subscriptions_controller.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class MailSubscriptionsController < ApplicationController + layout 'auth' + + skip_before_action :require_functional! + + before_action :set_body_classes + before_action :set_user + before_action :set_type + + def show; end + + def create + @user.settings[email_type_from_param] = false + @user.save! + end + + private + + def set_user + @user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe') + end + + def set_body_classes + @body_classes = 'lighter' + end + + def set_type + @type = email_type_from_param + end + + def email_type_from_param + case params[:type] + when 'follow', 'reblog', 'favourite', 'mention', 'follow_request' + "notification_emails.#{params[:type]}" + else + raise ArgumentError + end + end +end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 41e20add62..ac820e92bc 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -46,7 +46,7 @@ class MediaController < ApplicationController end def allow_iframing - response.headers['X-Frame-Options'] = 'ALLOWALL' + response.headers.delete('X-Frame-Options') end def set_pack diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index efae7e35f8..0a1df55066 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -10,6 +10,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :set_body_classes before_action :set_cache_headers + before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } + skip_before_action :require_functional! include Localized @@ -40,4 +42,14 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def set_cache_headers response.cache_control.replace(private: true, no_store: true) end + + def set_last_used_at_by_app + @last_used_at_by_app = Doorkeeper::AccessToken + .select('DISTINCT ON (application_id) application_id, last_used_at') + .where(resource_owner_id: current_resource_owner.id) + .where.not(last_used_at: nil) + .order(application_id: :desc, last_used_at: :desc) + .pluck(:application_id, :last_used_at) + .to_h + end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index bdbf8796fe..983caf22fa 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -12,6 +12,7 @@ class Settings::ImportsController < Settings::BaseController muting: 'muted_accounts_failures.csv', domain_blocking: 'blocked_domains_failures.csv', bookmarks: 'bookmarks_failures.csv', + lists: 'lists_failures.csv', }.freeze TYPE_TO_HEADERS_MAP = { @@ -20,6 +21,7 @@ class Settings::ImportsController < Settings::BaseController muting: ['Account address', 'Hide notifications'], domain_blocking: false, bookmarks: false, + lists: false, }.freeze def index @@ -49,6 +51,8 @@ class Settings::ImportsController < Settings::BaseController csv << [row.data['domain']] when :bookmarks csv << [row.data['uri']] + when :lists + csv << [row.data['list_name'], row.data['acct']] end end end diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb index faf778a7e5..c1f8b49898 100644 --- a/app/controllers/settings/preferences/base_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -19,6 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController end def user_params - params.require(:user).permit(:locale, chosen_languages: [], settings_attributes: UserSettings.keys) + params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys) end end diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb new file mode 100644 index 0000000000..fc4f23bb18 --- /dev/null +++ b/app/controllers/settings/verifications_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Settings::VerificationsController < Settings::BaseController + before_action :set_account + + def show + @verified_links = @account.fields.select(&:verified?) + end + + private + + def set_account + @account = current_account + end +end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 5758085988..0efafb8456 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -46,7 +46,7 @@ class StatusesController < ApplicationController return not_found if @status.hidden? || @status.reblog? expires_in 180, public: true - response.headers['X-Frame-Options'] = 'ALLOWALL' + response.headers.delete('X-Frame-Options') render layout: 'embedded' end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3192c7ab56..3148756b75 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -52,7 +52,7 @@ module ApplicationHelper if closed_registrations? || omniauth_only? 'https://joinmastodon.org/#getting-started' else - new_user_registration_path + ENV.fetch('SSO_ACCOUNT_SIGN_UP', new_user_registration_path) end end @@ -170,11 +170,11 @@ module ApplicationHelper end def storage_host - URI::HTTPS.build(host: storage_host_name).to_s + "https://#{storage_host_var}" end def storage_host? - storage_host_name.present? + storage_host_var.present? end def quote_wrap(text, line_width: 80, break_sequence: "\n") @@ -235,7 +235,7 @@ module ApplicationHelper private - def storage_host_name + def storage_host_var ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) end end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 00e1e5178b..840a18d3e6 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Metrics/ModuleLength - module LanguagesHelper ISO_639_1 = { aa: ['Afar', 'Afaraf'].freeze, diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb index fc08de13dd..ce616e8306 100644 --- a/app/helpers/react_component_helper.rb +++ b/app/helpers/react_component_helper.rb @@ -11,7 +11,7 @@ module ReactComponentHelper end def react_admin_component(name, props = {}) - data = { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) } + data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) } div_tag_with_data(data) end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 3d5592867c..889ca7f402 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,10 +5,6 @@ module SettingsHelper LanguagesHelper::SUPPORTED_LOCALES.keys end - def hash_to_object(hash) - HashObject.new(hash) - end - def session_device_icon(session) device = session.detection.device @@ -28,13 +24,4 @@ module SettingsHelper safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') end end - - def picture_hint(hint, picture) - if picture.original_filename.nil? - hint - else - link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete } - safe_join([hint, link], '
'.html_safe) - end - end end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index ac1b2f95fb..97b2f4e309 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -2,6 +2,7 @@ import 'packs/public-path'; import { delegate } from '@rails/ujs'; + import ready from '../mastodon/ready'; const setAnnouncementEndsAttributes = (target) => { diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index c6cf6704c3..40537377c0 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -1,9 +1,9 @@ // This file will be loaded on settings pages, regardless of theme. import 'packs/public-path'; +import { delegate } from '@rails/ujs'; import escapeTextContentForBrowser from 'escape-html'; -import { delegate } from '@rails/ujs'; import emojify from '../mastodon/features/emoji/emoji'; diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index b9144e43aa..30676dcf58 100644 --- a/app/javascript/core/theme.yml +++ b/app/javascript/core/theme.yml @@ -16,4 +16,5 @@ pack: modal: public.js public: public.js settings: settings.js + sign_up: share: diff --git a/app/javascript/core/two_factor_authentication.js b/app/javascript/core/two_factor_authentication.js index f076cdf30a..e76700a480 100644 --- a/app/javascript/core/two_factor_authentication.js +++ b/app/javascript/core/two_factor_authentication.js @@ -1,6 +1,8 @@ import 'packs/public-path'; -import axios from 'axios'; + import * as WebAuthnJSON from '@github/webauthn-json'; +import axios from 'axios'; + import ready from '../mastodon/ready'; import 'regenerator-runtime/runtime'; diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 9e5c85a15c..d4f18ff2d7 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index 586dcfd337..339c5f3adc 100644 --- a/app/javascript/flavours/glitch/actions/announcements.js +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -1,4 +1,5 @@ import api from '../api'; + import { normalizeAnnouncement } from './importer/normalizer'; export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/app.ts b/app/javascript/flavours/glitch/actions/app.ts index 46b2cdf93c..6fbfc07f68 100644 --- a/app/javascript/flavours/glitch/actions/app.ts +++ b/app/javascript/flavours/glitch/actions/app.ts @@ -1,8 +1,9 @@ import { createAction } from '@reduxjs/toolkit'; + import type { LayoutType } from '../is_mobile'; -type ChangeLayoutPayload = { +interface ChangeLayoutPayload { layout: LayoutType; -}; +} export const changeLayout = createAction('APP_LAYOUT_CHANGE'); diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js index 192aa3ce40..e293657ad3 100644 --- a/app/javascript/flavours/glitch/actions/blocks.js +++ b/app/javascript/flavours/glitch/actions/blocks.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; import { openModal } from './modal'; @@ -94,6 +95,6 @@ export function initBlockModal(account) { account, }); - dispatch(openModal('BLOCK')); + dispatch(openModal({ modalType: 'BLOCK' })); }; } diff --git a/app/javascript/flavours/glitch/actions/bookmarks.js b/app/javascript/flavours/glitch/actions/bookmarks.js index 3c8eec5468..0b16f61e63 100644 --- a/app/javascript/flavours/glitch/actions/bookmarks.js +++ b/app/javascript/flavours/glitch/actions/bookmarks.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/boosts.js b/app/javascript/flavours/glitch/actions/boosts.js index c0f0f3acc5..1fc2e391e2 100644 --- a/app/javascript/flavours/glitch/actions/boosts.js +++ b/app/javascript/flavours/glitch/actions/boosts.js @@ -14,7 +14,10 @@ export function initBoostModal(props) { privacy, }); - dispatch(openModal('BOOST', props)); + dispatch(openModal({ + modalType: 'BOOST', + modalProps: props, + })); }; } diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 26edd4155d..e43516de6a 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -1,11 +1,14 @@ +import { defineMessages } from 'react-intl'; + import axios from 'axios'; import { throttle } from 'lodash'; -import { defineMessages } from 'react-intl'; + import api from 'flavours/glitch/api'; import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light'; import { tagHistory } from 'flavours/glitch/settings'; import { recoverHashtags } from 'flavours/glitch/utils/hashtag'; import resizeImage from 'flavours/glitch/utils/resize_image'; + import { showAlert, showAlertForError } from './alerts'; import { useEmoji } from './emojis'; import { importFetchedAccounts, importFetchedStatus } from './importer'; @@ -436,7 +439,10 @@ export function initMediaEditModal(id) { id, }); - dispatch(openModal('FOCAL_POINT', { id })); + dispatch(openModal({ + modalType: 'FOCAL_POINT', + modalProps: { id }, + })); }; } diff --git a/app/javascript/flavours/glitch/actions/conversations.js b/app/javascript/flavours/glitch/actions/conversations.js index 4ef654b1f9..8c4c4529fb 100644 --- a/app/javascript/flavours/glitch/actions/conversations.js +++ b/app/javascript/flavours/glitch/actions/conversations.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedAccounts, importFetchedStatuses, diff --git a/app/javascript/flavours/glitch/actions/directory.js b/app/javascript/flavours/glitch/actions/directory.js index 4b2b6dd56d..cda63f2b5a 100644 --- a/app/javascript/flavours/glitch/actions/directory.js +++ b/app/javascript/flavours/glitch/actions/directory.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; diff --git a/app/javascript/flavours/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js index 7388e0c580..2d4d4e6206 100644 --- a/app/javascript/flavours/glitch/actions/favourites.js +++ b/app/javascript/flavours/glitch/actions/favourites.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/filters.js b/app/javascript/flavours/glitch/actions/filters.js index e9c609fc87..a11956ac56 100644 --- a/app/javascript/flavours/glitch/actions/filters.js +++ b/app/javascript/flavours/glitch/actions/filters.js @@ -1,4 +1,5 @@ import api from '../api'; + import { openModal } from './modal'; export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; @@ -14,9 +15,12 @@ export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS'; export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL'; export const initAddFilter = (status, { contextType }) => dispatch => - dispatch(openModal('FILTER', { - statusId: status?.get('id'), - contextType: contextType, + dispatch(openModal({ + modalType: 'FILTER', + modalProps: { + statusId: status?.get('id'), + contextType: contextType, + }, })); export const fetchFilters = () => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/history.js b/app/javascript/flavours/glitch/actions/history.js index c142aaf617..52401b7dce 100644 --- a/app/javascript/flavours/glitch/actions/history.js +++ b/app/javascript/flavours/glitch/actions/history.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedAccounts } from './importer'; export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 567db7eeb4..f9bf49f611 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -1,11 +1,12 @@ import escapeTextContentForBrowser from 'escape-html'; + import emojify from 'flavours/glitch/features/emoji/emoji'; -import { unescapeHTML } from 'flavours/glitch/utils/html'; import { autoHideCW } from 'flavours/glitch/utils/content_warning'; +import { unescapeHTML } from 'flavours/glitch/utils/html'; const domParser = new DOMParser(); -const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { +const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { obj[`:${emoji.shortcode}:`] = emoji; return obj; }, {}); @@ -19,7 +20,7 @@ export function searchTextFromRawStatus (status) { export function normalizeAccount(account) { account = { ...account }; - const emojiMap = makeEmojiMap(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); @@ -79,7 +80,7 @@ export function normalizeStatus(status, normalOldStatus, settings) { } else { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(normalStatus); + const emojiMap = makeEmojiMap(normalStatus.emojis); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); @@ -119,22 +120,48 @@ export function normalizeStatus(status, normalOldStatus, settings) { return normalStatus; } +export function normalizeStatusTranslation(translation, status) { + const emojiMap = makeEmojiMap(status.get('emojis').toJS()); + + const normalTranslation = { + detected_source_language: translation.detected_source_language, + language: translation.language, + provider: translation.provider, + contentHtml: emojify(translation.content, emojiMap), + spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap), + spoiler_text: translation.spoiler_text, + }; + + return normalTranslation; +} + export function normalizePoll(poll) { const normalPoll = { ...poll }; - const emojiMap = makeEmojiMap(normalPoll); + const emojiMap = makeEmojiMap(poll.emojis); normalPoll.options = poll.options.map((option, index) => ({ ...option, voted: poll.own_votes && poll.own_votes.includes(index), - title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); return normalPoll; } +export function normalizePollOptionTranslation(translation, poll) { + const emojiMap = makeEmojiMap(poll.get('emojis').toJS()); + + const normalTranslation = { + ...translation, + titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap), + }; + + return normalTranslation; +} + export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap(normalAnnouncement); + const emojiMap = makeEmojiMap(normalAnnouncement.emojis); normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index c7b552a656..6b8864a039 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedAccounts, importFetchedStatus } from './importer'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index 5ab9224363..b0789cd426 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { showAlertForError } from './alerts'; +import { importFetchedAccounts } from './importer'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; @@ -150,10 +151,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { dispatch(updateListSuccess(data)); if (shouldReset) { diff --git a/app/javascript/flavours/glitch/actions/local_settings.js b/app/javascript/flavours/glitch/actions/local_settings.js index adf7fd2abc..f2878daa50 100644 --- a/app/javascript/flavours/glitch/actions/local_settings.js +++ b/app/javascript/flavours/glitch/actions/local_settings.js @@ -1,4 +1,5 @@ import { expandSpoilers, disableSwiping } from 'flavours/glitch/initial_state'; + import { openModal } from './modal'; export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; @@ -27,9 +28,12 @@ export function checkDeprecatedLocalSettings() { } if (changed_settings.length > 0) { - dispatch(openModal('DEPRECATED_SETTINGS', { - settings: changed_settings, - onConfirm: () => dispatch(clearDeprecatedLocalSettings()), + dispatch(openModal({ + modalType: 'DEPRECATED_SETTINGS', + modalProps: { + settings: changed_settings, + onConfirm: () => dispatch(clearDeprecatedLocalSettings()), + }, })); } }; diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index 8fd9da405e..ccb1b23d6f 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,8 +1,10 @@ -import api from '../api'; -import { debounce } from 'lodash'; -import { compareId } from '../compare_id'; import { List as ImmutableList } from 'immutable'; +import { debounce } from 'lodash'; + +import api from '../api'; +import { compareId } from '../compare_id'; + export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; diff --git a/app/javascript/flavours/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js deleted file mode 100644 index ef2ae0e4c7..0000000000 --- a/app/javascript/flavours/glitch/actions/modal.js +++ /dev/null @@ -1,18 +0,0 @@ -export const MODAL_OPEN = 'MODAL_OPEN'; -export const MODAL_CLOSE = 'MODAL_CLOSE'; - -export function openModal(type, props) { - return { - type: MODAL_OPEN, - modalType: type, - modalProps: props, - }; -} - -export function closeModal(type, options = { ignoreFocus: false }) { - return { - type: MODAL_CLOSE, - modalType: type, - ignoreFocus: options.ignoreFocus, - }; -} diff --git a/app/javascript/flavours/glitch/actions/modal.ts b/app/javascript/flavours/glitch/actions/modal.ts new file mode 100644 index 0000000000..af34f5d6af --- /dev/null +++ b/app/javascript/flavours/glitch/actions/modal.ts @@ -0,0 +1,17 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root'; + +export type ModalType = keyof typeof MODAL_COMPONENTS; + +interface OpenModalPayload { + modalType: ModalType; + modalProps: unknown; +} +export const openModal = createAction('MODAL_OPEN'); + +interface CloseModalPayload { + modalType: ModalType | undefined; + ignoreFocus: boolean; +} +export const closeModal = createAction('MODAL_CLOSE'); diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js index aa47d14642..4af927d932 100644 --- a/app/javascript/flavours/glitch/actions/mutes.js +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -1,7 +1,9 @@ +import { openModal } from 'flavours/glitch/actions/modal'; + import api, { getLinks } from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; -import { openModal } from 'flavours/glitch/actions/modal'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; @@ -96,7 +98,7 @@ export function initMuteModal(account) { account, }); - dispatch(openModal('MUTE')); + dispatch(openModal({ modalType: 'MUTE' })); }; } diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index c044ea927f..a80746b756 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -1,5 +1,15 @@ +import { IntlMessageFormat } from 'intl-messageformat'; +import { defineMessages } from 'react-intl'; + +import { List as ImmutableList } from 'immutable'; + +import { compareId } from 'flavours/glitch/compare_id'; +import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; +import { unescapeHTML } from 'flavours/glitch/utils/html'; +import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; + import api, { getLinks } from '../api'; -import IntlMessageFormat from 'intl-messageformat'; + import { fetchFollowRequests, fetchRelationships } from './accounts'; import { importFetchedAccount, @@ -9,12 +19,9 @@ import { } from './importer'; import { submitMarkers } from './markers'; import { saveSettings } from './settings'; -import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; -import { unescapeHTML } from 'flavours/glitch/utils/html'; -import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; -import { compareId } from 'flavours/glitch/compare_id'; -import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; + + + export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; diff --git a/app/javascript/flavours/glitch/actions/onboarding.js b/app/javascript/flavours/glitch/actions/onboarding.js index 5038b7eb67..a4a525c427 100644 --- a/app/javascript/flavours/glitch/actions/onboarding.js +++ b/app/javascript/flavours/glitch/actions/onboarding.js @@ -6,7 +6,9 @@ export function showOnboardingOnce() { const alreadySeen = getState().getIn(['settings', 'onboarded']); if (!alreadySeen) { - dispatch(openModal('ONBOARDING')); + dispatch(openModal({ + modalType: 'ONBOARDING', + })); dispatch(changeSetting(['onboarded'], true)); dispatch(saveSettings()); } diff --git a/app/javascript/flavours/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js index d8c0a13737..8aca199e97 100644 --- a/app/javascript/flavours/glitch/actions/pin_statuses.js +++ b/app/javascript/flavours/glitch/actions/pin_statuses.js @@ -1,12 +1,14 @@ +import { me } from 'flavours/glitch/initial_state'; + import api from '../api'; + import { importFetchedStatuses } from './importer'; + export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; -import { me } from 'flavours/glitch/initial_state'; - export function fetchPinnedStatuses() { return (dispatch, getState) => { dispatch(fetchPinnedStatusesRequest()); diff --git a/app/javascript/flavours/glitch/actions/polls.js b/app/javascript/flavours/glitch/actions/polls.js index 8e8b82df5d..a37410dc90 100644 --- a/app/javascript/flavours/glitch/actions/polls.js +++ b/app/javascript/flavours/glitch/actions/polls.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedPoll } from './importer'; export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/push_notifications/index.js b/app/javascript/flavours/glitch/actions/push_notifications/index.js index 9dcc4bd4bb..46b63867f1 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/index.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/index.js @@ -1,5 +1,5 @@ -import { setAlerts } from './setter'; import { saveSettings } from './registerer'; +import { setAlerts } from './setter'; export function changeAlerts(path, value) { return dispatch => { diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js index bc5634233f..336bbc6869 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js @@ -1,5 +1,6 @@ import api from '../../api'; import { pushNotificationsSetting } from '../../settings'; + import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; // Taken from https://www.npmjs.com/package/web-push diff --git a/app/javascript/flavours/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js index fbe5b3791b..756b8cd05e 100644 --- a/app/javascript/flavours/glitch/actions/reports.js +++ b/app/javascript/flavours/glitch/actions/reports.js @@ -1,4 +1,5 @@ import api from '../api'; + import { openModal } from './modal'; export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; @@ -6,9 +7,12 @@ export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; export const initReport = (account, status) => dispatch => - dispatch(openModal('REPORT', { - accountId: account.get('id'), - statusId: status?.get('id'), + dispatch(openModal({ + modalType: 'REPORT', + modalProps: { + accountId: account.get('id'), + statusId: status?.get('id'), + }, })); export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index 0012808e5b..d5154c6a84 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -1,4 +1,5 @@ import api from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts, importFetchedStatuses } from './importer'; diff --git a/app/javascript/flavours/glitch/actions/server.js b/app/javascript/flavours/glitch/actions/server.js index 091af0f0fe..65f3efc3a7 100644 --- a/app/javascript/flavours/glitch/actions/server.js +++ b/app/javascript/flavours/glitch/actions/server.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedAccount } from './importer'; export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; @@ -18,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; export const fetchServer = () => (dispatch, getState) => { + if (getState().getIn(['server', 'server', 'isLoading'])) { + return; + } + dispatch(fetchServerRequest()); api(getState) @@ -65,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({ }); export const fetchExtendedDescription = () => (dispatch, getState) => { + if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) { + return; + } + dispatch(fetchExtendedDescriptionRequest()); api(getState) @@ -88,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({ }); export const fetchDomainBlocks = () => (dispatch, getState) => { + if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) { + return; + } + dispatch(fetchDomainBlocksRequest()); api(getState) diff --git a/app/javascript/flavours/glitch/actions/settings.js b/app/javascript/flavours/glitch/actions/settings.js index 60f0abf950..120ae133ed 100644 --- a/app/javascript/flavours/glitch/actions/settings.js +++ b/app/javascript/flavours/glitch/actions/settings.js @@ -1,5 +1,7 @@ -import api from '../api'; import { debounce } from 'lodash'; + +import api from '../api'; + import { showAlertForError } from './alerts'; export const SETTING_CHANGE = 'SETTING_CHANGE'; diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 487cd69884..5bdd31c343 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -1,8 +1,8 @@ import api from '../api'; -import { deleteFromTimelines } from './timelines'; -import { importFetchedStatus, importFetchedStatuses } from './importer'; import { ensureComposeIsVisible, setComposeToStatus } from './compose'; +import { importFetchedStatus, importFetchedStatuses } from './importer'; +import { deleteFromTimelines } from './timelines'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -344,7 +344,8 @@ export const translateStatusFail = (id, error) => ({ error, }); -export const undoStatusTranslation = id => ({ +export const undoStatusTranslation = (id, pollId) => ({ type: STATUS_TRANSLATE_UNDO, id, + pollId, }); diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js index 137b68e222..e57b37a122 100644 --- a/app/javascript/flavours/glitch/actions/store.js +++ b/app/javascript/flavours/glitch/actions/store.js @@ -1,4 +1,5 @@ import { Iterable, fromJS } from 'immutable'; + import { hydrateCompose } from './compose'; import { importFetchedAccounts } from './importer'; import { saveSettings } from './settings'; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index b4eb60ad4e..f1c44d2e29 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -1,6 +1,18 @@ // @ts-check +import { getLocale } from 'flavours/glitch/locales'; + import { connectStream } from '../stream'; + +import { + fetchAnnouncements, + updateAnnouncements, + updateReaction as updateAnnouncementsReaction, + deleteAnnouncement, +} from './announcements'; +import { updateConversations } from './conversations'; +import { updateNotifications, expandNotifications } from './notifications'; +import { updateStatus } from './statuses'; import { updateTimeline, deleteFromTimelines, @@ -12,18 +24,6 @@ import { fillCommunityTimelineGaps, fillListTimelineGaps, } from './timelines'; -import { updateNotifications, expandNotifications } from './notifications'; -import { updateConversations } from './conversations'; -import { updateStatus } from './statuses'; -import { - fetchAnnouncements, - updateAnnouncements, - updateReaction as updateAnnouncementsReaction, - deleteAnnouncement, -} from './announcements'; -import { getLocale } from 'mastodon/locales'; - -const { messages } = getLocale(); /** * @param {number} max @@ -42,8 +42,10 @@ const randomUpTo = max => * @param {function(object): boolean} [options.accept] * @returns {function(): void} */ -export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => - connectStream(channelName, params, (dispatch, getState) => { +export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => { + const { messages } = getLocale(); + + return connectStream(channelName, params, (dispatch, getState) => { const locale = getState().getIn(['meta', 'locale']); // @ts-expect-error @@ -120,6 +122,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, }; }); +}; /** * @param {Function} dispatch diff --git a/app/javascript/flavours/glitch/actions/suggestions.js b/app/javascript/flavours/glitch/actions/suggestions.js index 9e8cd1ea40..870a311024 100644 --- a/app/javascript/flavours/glitch/actions/suggestions.js +++ b/app/javascript/flavours/glitch/actions/suggestions.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 603759760b..7d4d56a784 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,11 +1,13 @@ -import { importFetchedStatus, importFetchedStatuses } from './importer'; -import { submitMarkers } from './markers'; -import api, { getLinks } from 'flavours/glitch/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +import api, { getLinks } from 'flavours/glitch/api'; import { compareId } from 'flavours/glitch/compare_id'; import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; import { toServerSideType } from 'flavours/glitch/utils/filters'; +import { importFetchedStatus, importFetchedStatuses } from './importer'; +import { submitMarkers } from './markers'; + export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; diff --git a/app/javascript/flavours/glitch/actions/trends.js b/app/javascript/flavours/glitch/actions/trends.js index edda0b5b5d..d314423884 100644 --- a/app/javascript/flavours/glitch/actions/trends.js +++ b/app/javascript/flavours/glitch/actions/trends.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js index 42b64d6cc5..948ffbc95c 100644 --- a/app/javascript/flavours/glitch/api.js +++ b/app/javascript/flavours/glitch/api.js @@ -2,8 +2,8 @@ import axios from 'axios'; import LinkHeader from 'http-link-header'; -import ready from './ready'; +import ready from './ready'; /** * @param {import('axios').AxiosResponse} response * @returns {LinkHeader} diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx index e9f04fcda7..518464b040 100644 --- a/app/javascript/flavours/glitch/components/account.jsx +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -1,20 +1,24 @@ -import React, { Fragment } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { Avatar } from './avatar'; -import DisplayName from './display_name'; -import Permalink from './permalink'; -import { IconButton } from './icon_button'; + import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Skeleton } from 'flavours/glitch/components/skeleton'; import { me } from 'flavours/glitch/initial_state'; + +import { Avatar } from './avatar'; +import { DisplayName } from './display_name'; +import { IconButton } from './icon_button'; +import Permalink from './permalink'; import { RelativeTimestamp } from './relative_timestamp'; -import Skeleton from 'flavours/glitch/components/skeleton'; + const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, @@ -97,10 +101,10 @@ class Account extends ImmutablePureComponent { if (hidden) { return ( - + <> {account.get('display_name')} {account.get('username')} - + ); } @@ -128,10 +132,10 @@ class Account extends ImmutablePureComponent { hidingNotificationsButton = ; } buttons = ( - + <> {hidingNotificationsButton} - + ); } else if (defaultAction === 'mute') { buttons = ; diff --git a/app/javascript/flavours/glitch/components/admin/Counter.jsx b/app/javascript/flavours/glitch/components/admin/Counter.jsx index 5b6a19f8da..9bb792fc9d 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.jsx +++ b/app/javascript/flavours/glitch/components/admin/Counter.jsx @@ -1,10 +1,14 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; +import { PureComponent } from 'react'; + import { FormattedNumber } from 'react-intl'; -import { Sparklines, SparklinesCurve } from 'react-sparklines'; + import classNames from 'classnames'; -import Skeleton from 'flavours/glitch/components/skeleton'; + +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; const percIncrease = (a, b) => { let percent; @@ -24,7 +28,7 @@ const percIncrease = (a, b) => { return percent; }; -export default class Counter extends React.PureComponent { +export default class Counter extends PureComponent { static propTypes = { measure: PropTypes.string.isRequired, @@ -62,25 +66,25 @@ export default class Counter extends React.PureComponent { if (loading) { content = ( - + <> - + ); } else { const measure = data[0]; const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1); content = ( - + <> {measure.human_value || } {measure.previous_total && ( 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'})} - + ); } const inner = ( - + <>

{content}
@@ -96,7 +100,7 @@ export default class Counter extends React.PureComponent { )} - + ); if (href) { diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.jsx b/app/javascript/flavours/glitch/components/admin/Dimension.jsx index 3dac8c6c24..793fe2dd76 100644 --- a/app/javascript/flavours/glitch/components/admin/Dimension.jsx +++ b/app/javascript/flavours/glitch/components/admin/Dimension.jsx @@ -1,11 +1,13 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; -import { FormattedNumber } from 'react-intl'; -import { roundTo10 } from 'flavours/glitch/utils/numbers'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { PureComponent } from 'react'; -export default class Dimension extends React.PureComponent { +import { FormattedNumber } from 'react-intl'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; +import { roundTo10 } from 'flavours/glitch/utils/numbers'; + +export default class Dimension extends PureComponent { static propTypes = { dimension: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx b/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx new file mode 100644 index 0000000000..9ec1460fcf --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedNumber, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; + +export default class ImpactReport extends PureComponent { + + static propTypes = { + domain: PropTypes.string.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { domain } = this.props; + + const params = { + domain: domain, + include_subdomains: true, + }; + + api().post('/api/v1/admin/measures', { + keys: ['instance_accounts', 'instance_follows', 'instance_followers'], + start_at: null, + end_at: null, + instance_accounts: params, + instance_follows: params, + instance_followers: params, + }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + + return ( +
+

+ + + + + + + + + + 0 })}> + + + + + + 0 })}> + + + + + +
+ + + {loading ? : } +
+ + + {loading ? : } +
+ + + {loading ? : } +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx index ecefe7a840..d72465e4ad 100644 --- a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx +++ b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx @@ -1,16 +1,19 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; +import { PureComponent } from 'react'; + import { injectIntl, defineMessages } from 'react-intl'; + import classNames from 'classnames'; +import api from 'flavours/glitch/api'; + const messages = defineMessages({ other: { id: 'report.categories.other', defaultMessage: 'Other' }, spam: { id: 'report.categories.spam', defaultMessage: 'Spam' }, violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' }, }); -class Category extends React.PureComponent { +class Category extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -52,7 +55,7 @@ class Category extends React.PureComponent { } -class Rule extends React.PureComponent { +class Rule extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -84,7 +87,7 @@ class Rule extends React.PureComponent { } -class ReportReasonSelector extends React.PureComponent { +class ReportReasonSelector extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/components/admin/Retention.jsx b/app/javascript/flavours/glitch/components/admin/Retention.jsx index e1ba3f6c9d..2cfc30b6fb 100644 --- a/app/javascript/flavours/glitch/components/admin/Retention.jsx +++ b/app/javascript/flavours/glitch/components/admin/Retention.jsx @@ -1,8 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; +import { PureComponent } from 'react'; + import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; + import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; import { roundTo10 } from 'flavours/glitch/utils/numbers'; const dateForCohort = cohort => { @@ -14,7 +17,7 @@ const dateForCohort = cohort => { } }; -export default class Retention extends React.PureComponent { +export default class Retention extends PureComponent { static propTypes = { start_at: PropTypes.string, diff --git a/app/javascript/flavours/glitch/components/admin/Trends.jsx b/app/javascript/flavours/glitch/components/admin/Trends.jsx index 774bf36e6e..975ea6e0f2 100644 --- a/app/javascript/flavours/glitch/components/admin/Trends.jsx +++ b/app/javascript/flavours/glitch/components/admin/Trends.jsx @@ -1,11 +1,14 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; + import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; import Hashtag from 'flavours/glitch/components/hashtag'; -export default class Trends extends React.PureComponent { +export default class Trends extends PureComponent { static propTypes = { limit: PropTypes.number.isRequired, diff --git a/app/javascript/flavours/glitch/components/animated_number.tsx b/app/javascript/flavours/glitch/components/animated_number.tsx index f6c77d35ff..81e0af395b 100644 --- a/app/javascript/flavours/glitch/components/animated_number.tsx +++ b/app/javascript/flavours/glitch/components/animated_number.tsx @@ -1,8 +1,12 @@ -import React, { useCallback, useState } from 'react'; -import ShortNumber from './short_number'; +import { useCallback, useState } from 'react'; +import * as React from 'react'; + import { TransitionMotion, spring } from 'react-motion'; + import { reduceMotion } from '../initial_state'; +import ShortNumber from './short_number'; + const obfuscatedCount = (count: number) => { if (count < 0) { return 0; @@ -13,10 +17,10 @@ const obfuscatedCount = (count: number) => { } }; -type Props = { +interface Props { value: number; obfuscate?: boolean; -}; +} export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { const [previousValue, setPreviousValue] = useState(value); const [direction, setDirection] = useState<1 | -1>(1); @@ -64,7 +68,11 @@ export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { transform: `translateY(${style.y * 100}%)`, }} > - {obfuscate ? obfuscatedCount(data) : } + {obfuscate ? ( + obfuscatedCount(data as number) + ) : ( + + )} ))} diff --git a/app/javascript/flavours/glitch/components/attachment_list.jsx b/app/javascript/flavours/glitch/components/attachment_list.jsx index 92ba98126a..173157b0d5 100644 --- a/app/javascript/flavours/glitch/components/attachment_list.jsx +++ b/app/javascript/flavours/glitch/components/attachment_list.jsx @@ -1,9 +1,12 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { FormattedMessage } from 'react-intl'; + import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + import { Icon } from 'flavours/glitch/components/icon'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx index 83fafbd10d..32a996fd7c 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx @@ -1,10 +1,10 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; +import { PureComponent } from 'react'; +import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; import { assetHost } from 'flavours/glitch/utils/config'; -export default class AutosuggestEmoji extends React.PureComponent { +export default class AutosuggestEmoji extends PureComponent { static propTypes = { emoji: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx b/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx deleted file mode 100644 index d787ed07ad..0000000000 --- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ShortNumber from 'flavours/glitch/components/short_number'; -import { FormattedMessage } from 'react-intl'; - -export default class AutosuggestHashtag extends React.PureComponent { - - static propTypes = { - tag: PropTypes.shape({ - name: PropTypes.string.isRequired, - url: PropTypes.string, - history: PropTypes.array, - }).isRequired, - }; - - render() { - const { tag } = this.props; - const weeklyUses = tag.history && ( - total + day.uses * 1, 0)} - /> - ); - - return ( -
-
- #{tag.name} -
- {tag.history !== undefined && ( -
- -
- )} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx new file mode 100644 index 0000000000..932370884a --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx @@ -0,0 +1,42 @@ +import { FormattedMessage } from 'react-intl'; + +import ShortNumber from 'flavours/glitch/components/short_number'; + +interface Props { + tag: { + name: string; + url?: string; + history?: Array<{ + uses: number; + accounts: string; + day: string; + }>; + following?: boolean; + type: 'hashtag'; + }; +} + +export const AutosuggestHashtag: React.FC = ({ tag }) => { + const weeklyUses = tag.history && ( + total + day.uses * 1, 0)} + /> + ); + + return ( +
+
+ #{tag.name} +
+ {tag.history !== undefined && ( +
+ +
+ )} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.jsx b/app/javascript/flavours/glitch/components/autosuggest_input.jsx index ea9fd0828a..f0833c8c6b 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -1,12 +1,15 @@ -import React from 'react'; -import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; -import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import classNames from 'classnames'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; + +import AutosuggestEmoji from './autosuggest_emoji'; +import { AutosuggestHashtag } from './autosuggest_hashtag'; + const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { let word; @@ -154,7 +157,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { this.input.focus(); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx index a016e44b72..25ca3fefa5 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx @@ -1,13 +1,17 @@ -import React from 'react'; -import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; -import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Textarea from 'react-textarea-autosize'; + import classNames from 'classnames'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import Textarea from 'react-textarea-autosize'; + +import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; + +import AutosuggestEmoji from './autosuggest_emoji'; +import { AutosuggestHashtag } from './autosuggest_hashtag'; + const textAtCursorMatchesToken = (str, caretPosition) => { let word; @@ -153,7 +157,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { this.textarea.focus(); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } diff --git a/app/javascript/flavours/glitch/components/avatar.tsx b/app/javascript/flavours/glitch/components/avatar.tsx index 1bd7739f5f..11253a8e94 100644 --- a/app/javascript/flavours/glitch/components/avatar.tsx +++ b/app/javascript/flavours/glitch/components/avatar.tsx @@ -1,16 +1,18 @@ import * as React from 'react'; + import classNames from 'classnames'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; + import { useHovering } from 'flavours/glitch/hooks/useHovering'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; import type { Account } from 'flavours/glitch/types/resources'; -type Props = { +interface Props { account: Account | undefined; className?: string; size: number; style?: React.CSSProperties; inline?: boolean; -}; +} export const Avatar: React.FC = ({ account, diff --git a/app/javascript/flavours/glitch/components/avatar_composite.jsx b/app/javascript/flavours/glitch/components/avatar_composite.jsx index 1c23a8b36e..5503abf4a9 100644 --- a/app/javascript/flavours/glitch/components/avatar_composite.jsx +++ b/app/javascript/flavours/glitch/components/avatar_composite.jsx @@ -1,9 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; + import { autoPlayGif } from 'flavours/glitch/initial_state'; -export default class AvatarComposite extends React.PureComponent { +export default class AvatarComposite extends PureComponent { static propTypes = { accounts: ImmutablePropTypes.list.isRequired, diff --git a/app/javascript/flavours/glitch/components/avatar_overlay.jsx b/app/javascript/flavours/glitch/components/avatar_overlay.jsx index 01dec587a5..d8215a4785 100644 --- a/app/javascript/flavours/glitch/components/avatar_overlay.jsx +++ b/app/javascript/flavours/glitch/components/avatar_overlay.jsx @@ -1,9 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; + import { autoPlayGif } from 'flavours/glitch/initial_state'; -export default class AvatarOverlay extends React.PureComponent { +export default class AvatarOverlay extends PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/components/blurhash.tsx b/app/javascript/flavours/glitch/components/blurhash.tsx index 7005136765..d98e7d35db 100644 --- a/app/javascript/flavours/glitch/components/blurhash.tsx +++ b/app/javascript/flavours/glitch/components/blurhash.tsx @@ -1,14 +1,15 @@ -import { decode } from 'blurhash'; -import React, { useRef, useEffect } from 'react'; +import { useRef, useEffect } from 'react'; +import * as React from 'react'; -type Props = { +import { decode } from 'blurhash'; + +interface Props extends React.HTMLAttributes { hash: string; width?: number; height?: number; dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched children?: never; - [key: string]: any; -}; +} const Blurhash: React.FC = ({ hash, width = 32, @@ -21,6 +22,7 @@ const Blurhash: React.FC = ({ useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const canvas = canvasRef.current!; + // eslint-disable-next-line no-self-assign canvas.width = canvas.width; // resets canvas diff --git a/app/javascript/flavours/glitch/components/button.jsx b/app/javascript/flavours/glitch/components/button.jsx index 40b8f5a159..bdeeeac999 100644 --- a/app/javascript/flavours/glitch/components/button.jsx +++ b/app/javascript/flavours/glitch/components/button.jsx @@ -1,8 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import classNames from 'classnames'; -export default class Button extends React.PureComponent { +export default class Button extends PureComponent { static propTypes = { text: PropTypes.node, diff --git a/app/javascript/flavours/glitch/components/check.jsx b/app/javascript/flavours/glitch/components/check.jsx index ee2ef1595a..d818480b7b 100644 --- a/app/javascript/flavours/glitch/components/check.jsx +++ b/app/javascript/flavours/glitch/components/check.jsx @@ -1,5 +1,3 @@ -import React from 'react'; - const Check = () => ( diff --git a/app/javascript/flavours/glitch/components/circular_progress.tsx b/app/javascript/flavours/glitch/components/circular_progress.tsx new file mode 100644 index 0000000000..850eb93e48 --- /dev/null +++ b/app/javascript/flavours/glitch/components/circular_progress.tsx @@ -0,0 +1,27 @@ +interface Props { + size: number; + strokeWidth: number; +} + +export const CircularProgress: React.FC = ({ size, strokeWidth }) => { + const viewBox = `0 0 ${size} ${size}`; + const radius = (size - strokeWidth) / 2; + + return ( + + + + ); +}; diff --git a/app/javascript/flavours/glitch/components/column.jsx b/app/javascript/flavours/glitch/components/column.jsx index 47293ef18e..312a6848b5 100644 --- a/app/javascript/flavours/glitch/components/column.jsx +++ b/app/javascript/flavours/glitch/components/column.jsx @@ -1,9 +1,13 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { supportsPassiveEvents } from 'detect-passive-events'; + import { scrollTop } from '../scroll'; -export default class Column extends React.PureComponent { +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +export default class Column extends PureComponent { static propTypes = { children: PropTypes.node, @@ -37,17 +41,17 @@ export default class Column extends React.PureComponent { componentDidMount () { if (this.props.bindToDocument) { - document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { - this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, listenerOptions); } } componentWillUnmount () { if (this.props.bindToDocument) { - document.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('wheel', this.handleWheel, listenerOptions); } else { - this.node.removeEventListener('wheel', this.handleWheel); + this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } } diff --git a/app/javascript/flavours/glitch/components/column_back_button.jsx b/app/javascript/flavours/glitch/components/column_back_button.jsx index 9a3afe8b3d..0934d4b335 100644 --- a/app/javascript/flavours/glitch/components/column_back_button.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button.jsx @@ -1,10 +1,13 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import { Icon } from 'flavours/glitch/components/icon'; +import { PureComponent } from 'react'; import { createPortal } from 'react-dom'; -export default class ColumnBackButton extends React.PureComponent { +import { FormattedMessage } from 'react-intl'; + +import { Icon } from 'flavours/glitch/components/icon'; + + +export default class ColumnBackButton extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -14,17 +17,15 @@ export default class ColumnBackButton extends React.PureComponent { multiColumn: PropTypes.bool, }; - handleClick = (event) => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history.state) { - const state = this.context.router.history.location.state; - if (event.shiftKey && state && state.mastodonBackSteps) { - this.context.router.history.go(-state.mastodonBackSteps); - } else { - this.context.router.history.goBack(); - } + handleClick = () => { + const { router } = this.context; + + // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201 + // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location + if (router.route.location.key) { + router.history.goBack(); } else { - this.context.router.history.push('/'); + router.history.push('/'); } }; diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx index b27ead1c73..7b3bac45f9 100644 --- a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx @@ -1,25 +1,25 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedMessage } from 'react-intl'; + import { Icon } from 'flavours/glitch/components/icon'; -export default class ColumnBackButtonSlim extends React.PureComponent { +export default class ColumnBackButtonSlim extends PureComponent { static contextTypes = { router: PropTypes.object, }; - handleClick = (event) => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history.state) { - const state = this.context.router.history.location.state; - if (event.shiftKey && state && state.mastodonBackSteps) { - this.context.router.history.go(-state.mastodonBackSteps); - } else { - this.context.router.history.goBack(); - } + handleClick = () => { + const { router } = this.context; + + // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201 + // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location + if (router.route.location.key) { + router.history.goBack(); } else { - this.context.router.history.push('/'); + router.history.push('/'); } }; diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx index bd26605954..e8c056c0bf 100644 --- a/app/javascript/flavours/glitch/components/column_header.jsx +++ b/app/javascript/flavours/glitch/components/column_header.jsx @@ -1,8 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; import { createPortal } from 'react-dom'; -import classNames from 'classnames'; + import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; + +import classNames from 'classnames'; + import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ @@ -12,7 +15,7 @@ const messages = defineMessages({ moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, }); -class ColumnHeader extends React.PureComponent { +class ColumnHeader extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -42,20 +45,6 @@ class ColumnHeader extends React.PureComponent { animating: false, }; - historyBack = (skip) => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history.state) { - const state = this.context.router.history.location.state; - if (skip && state && state.mastodonBackSteps) { - this.context.router.history.go(-state.mastodonBackSteps); - } else { - this.context.router.history.goBack(); - } - } else { - this.context.router.history.push('/'); - } - }; - handleToggleClick = (e) => { e.stopPropagation(); this.setState({ collapsed: !this.state.collapsed, animating: true }); @@ -73,8 +62,16 @@ class ColumnHeader extends React.PureComponent { this.props.onMove(1); }; - handleBackClick = (event) => { - this.historyBack(event.shiftKey); + handleBackClick = () => { + const { router } = this.context; + + // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201 + // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location + if (router.route.location.key) { + router.history.goBack(); + } else { + router.history.push('/'); + } }; handleTransitionEnd = () => { @@ -83,8 +80,9 @@ class ColumnHeader extends React.PureComponent { handlePin = () => { if (!this.props.pinned) { - this.historyBack(); + this.context.router.history.replace('/'); } + this.props.onPin(); }; diff --git a/app/javascript/flavours/glitch/components/common_counter.jsx b/app/javascript/flavours/glitch/components/common_counter.jsx index 9964be5166..785907bd25 100644 --- a/app/javascript/flavours/glitch/components/common_counter.jsx +++ b/app/javascript/flavours/glitch/components/common_counter.jsx @@ -1,7 +1,5 @@ // @ts-check -import React from 'react'; import { FormattedMessage } from 'react-intl'; - /** * Returns custom renderer for one of the common counter types * @param {"statuses" | "following" | "followers"} counterType diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.jsx b/app/javascript/flavours/glitch/components/dismissable_banner.jsx index 2aed19b88f..21063c9ed6 100644 --- a/app/javascript/flavours/glitch/components/dismissable_banner.jsx +++ b/app/javascript/flavours/glitch/components/dismissable_banner.jsx @@ -1,14 +1,17 @@ -import React from 'react'; -import { IconButton } from './icon_button'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { injectIntl, defineMessages } from 'react-intl'; + import { bannerSettings } from 'flavours/glitch/settings'; +import { IconButton } from './icon_button'; + const messages = defineMessages({ dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' }, }); -class DismissableBanner extends React.PureComponent { +class DismissableBanner extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/components/display_name.jsx b/app/javascript/flavours/glitch/components/display_name.jsx deleted file mode 100644 index fceca5d961..0000000000 --- a/app/javascript/flavours/glitch/components/display_name.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; -import Skeleton from 'flavours/glitch/components/skeleton'; - -export default class DisplayName extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - others: ImmutablePropTypes.list, - localDomain: PropTypes.string, - inline: PropTypes.bool, - }; - - handleMouseEnter = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - }; - - handleMouseLeave = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - }; - - render () { - const { others, localDomain, inline } = this.props; - - let displayName, suffix, account; - - if (others && others.size > 1) { - displayName = others.take(2).map(a => ).reduce((prev, cur) => [prev, ', ', cur]); - - if (others.size - 2 > 0) { - suffix = `+${others.size - 2}`; - } - } else if ((others && others.size > 0) || this.props.account) { - if (others && others.size > 0) { - account = others.first(); - } else { - account = this.props.account; - } - - let acct = account.get('acct'); - - if (acct.indexOf('@') === -1 && localDomain) { - acct = `${acct}@${localDomain}`; - } - - displayName = ; - suffix = @{acct}; - } else { - displayName = ; - suffix = ; - } - - return ( - - {displayName} - {inline ? ' ' : null} - {suffix} - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/display_name.tsx b/app/javascript/flavours/glitch/components/display_name.tsx new file mode 100644 index 0000000000..7224ac3d73 --- /dev/null +++ b/app/javascript/flavours/glitch/components/display_name.tsx @@ -0,0 +1,124 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import type { List } from 'immutable'; + +import type { Account } from 'flavours/glitch/types/resources'; + +import { autoPlayGif } from '../initial_state'; + +import { Skeleton } from './skeleton'; + +interface Props { + account: Account; + others: List; + localDomain: string; + inline?: boolean; +} +export class DisplayName extends React.PureComponent { + handleMouseEnter: React.ReactEventHandler = ({ + currentTarget, + }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('img.custom-emoji'); + + emojis.forEach((emoji) => { + const originalSrc = emoji.getAttribute('data-original'); + if (originalSrc != null) emoji.src = originalSrc; + }); + }; + + handleMouseLeave: React.ReactEventHandler = ({ + currentTarget, + }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('img.custom-emoji'); + + emojis.forEach((emoji) => { + const staticSrc = emoji.getAttribute('data-static'); + if (staticSrc != null) emoji.src = staticSrc; + }); + }; + + render() { + const { others, localDomain, inline } = this.props; + + let displayName: React.ReactNode, suffix: React.ReactNode, account: Account; + + if (others && others.size > 1) { + displayName = others + .take(2) + .map((a) => ( + + + + )) + .reduce((prev, cur) => [prev, ', ', cur]); + + if (others.size - 2 > 0) { + suffix = `+${others.size - 2}`; + } + } else if ((others && others.size > 0) || this.props.account) { + if (others && others.size > 0) { + account = others.first(); + } else { + account = this.props.account; + } + + let acct = account.get('acct'); + + if (acct.indexOf('@') === -1 && localDomain) { + acct = `${acct}@${localDomain}`; + } + + displayName = ( + + + + ); + suffix = @{acct}; + } else { + displayName = ( + + + + + + ); + suffix = ( + + + + ); + } + + return ( + + {displayName} + {inline ? ' ' : null} + {suffix} + + ); + } +} diff --git a/app/javascript/flavours/glitch/components/domain.tsx b/app/javascript/flavours/glitch/components/domain.tsx index af0fec35af..50c5c256ec 100644 --- a/app/javascript/flavours/glitch/components/domain.tsx +++ b/app/javascript/flavours/glitch/components/domain.tsx @@ -1,6 +1,9 @@ -import React, { useCallback } from 'react'; +import { useCallback } from 'react'; +import * as React from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + import { IconButton } from './icon_button'; -import { InjectedIntl, defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ unblockDomain: { @@ -9,12 +12,14 @@ const messages = defineMessages({ }, }); -type Props = { +interface Props { domain: string; onUnblockDomain: (domain: string) => void; - intl: InjectedIntl; -}; -const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { +} + +export const Domain: React.FC = ({ domain, onUnblockDomain }) => { + const intl = useIntl(); + const handleDomainUnblock = useCallback(() => { onUnblockDomain(domain); }, [domain, onUnblockDomain]); @@ -38,5 +43,3 @@ const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { ); }; - -export const Domain = injectIntl(_Domain); diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx index 3f17bc1299..0416df5d45 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx @@ -1,16 +1,20 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { IconButton } from './icon_button'; -import Overlay from 'react-overlays/Overlay'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import classNames from 'classnames'; -import { CircularProgress } from 'flavours/glitch/components/loading_indicator'; +import { PureComponent, cloneElement, Children } from 'react'; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import { supportsPassiveEvents } from 'detect-passive-events'; +import Overlay from 'react-overlays/Overlay'; + +import { CircularProgress } from "./circular_progress"; +import { IconButton } from './icon_button'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; let id = 0; -class DropdownMenu extends React.PureComponent { +class DropdownMenu extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -35,12 +39,13 @@ class DropdownMenu extends React.PureComponent { handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('keydown', this.handleKeyDown, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('keydown', this.handleKeyDown, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem && this.props.openedViaKeyboard) { @@ -49,8 +54,8 @@ class DropdownMenu extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('keydown', this.handleKeyDown, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -115,10 +120,10 @@ class DropdownMenu extends React.PureComponent { return
  • ; } - const { text, href = '#', target = '_blank', method } = option; + const { text, href = '#', target = '_blank', method, dangerous } = option; return ( -
  • +
  • {text} @@ -154,7 +159,7 @@ class DropdownMenu extends React.PureComponent { } -export default class Dropdown extends React.PureComponent { +export default class Dropdown extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -285,7 +290,7 @@ export default class Dropdown extends React.PureComponent { const open = this.state.id === openDropdownId; - const button = children ? React.cloneElement(React.Children.only(children), { + const button = children ? cloneElement(Children.only(children), { onClick: this.handleClick, onMouseDown: this.handleMouseDown, onKeyDown: this.handleButtonKeyDown, @@ -305,7 +310,7 @@ export default class Dropdown extends React.PureComponent { ); return ( - + <> {button} @@ -328,7 +333,7 @@ export default class Dropdown extends React.PureComponent { )} - + ); } diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js index a1519757de..7c9c167137 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js @@ -1,4 +1,5 @@ import { connect } from 'react-redux'; + import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; import { fetchHistory } from 'flavours/glitch/actions/history'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx index 5315b812fc..3dbac58b54 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx +++ b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx @@ -1,22 +1,29 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { FormattedMessage, injectIntl } from 'react-intl'; -import { Icon } from 'flavours/glitch/components/icon'; -import DropdownMenu from './containers/dropdown_menu_container'; + import { connect } from 'react-redux'; + import { openModal } from 'flavours/glitch/actions/modal'; -import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; +import { Icon } from 'flavours/glitch/components/icon'; import InlineAccount from 'flavours/glitch/components/inline_account'; +import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; + +import DropdownMenu from './containers/dropdown_menu_container'; const mapDispatchToProps = (dispatch, { statusId }) => ({ onItemClick (index) { - dispatch(openModal('COMPARE_HISTORY', { index, statusId })); + dispatch(openModal({ + modalType: 'COMPARE_HISTORY', + modalProps: { index, statusId }, + })); }, }); -class EditedTimestamp extends React.PureComponent { +class EditedTimestamp extends PureComponent { static propTypes = { statusId: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/components/error_boundary.jsx b/app/javascript/flavours/glitch/components/error_boundary.jsx index 234a53417f..4a4dadf0a5 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.jsx +++ b/app/javascript/flavours/glitch/components/error_boundary.jsx @@ -1,12 +1,16 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; -import { source_url } from 'flavours/glitch/initial_state'; -import { preferencesLink } from 'flavours/glitch/utils/backend_links'; -import StackTrace from 'stacktrace-js'; + import { Helmet } from 'react-helmet'; -export default class ErrorBoundary extends React.PureComponent { +import StackTrace from 'stacktrace-js'; + +import { source_url } from 'flavours/glitch/initial_state'; +import { preferencesLink } from 'flavours/glitch/utils/backend_links'; + +export default class ErrorBoundary extends PureComponent { static propTypes = { children: PropTypes.node, diff --git a/app/javascript/flavours/glitch/components/gifv.tsx b/app/javascript/flavours/glitch/components/gifv.tsx index 72914ba741..c9b9de29b6 100644 --- a/app/javascript/flavours/glitch/components/gifv.tsx +++ b/app/javascript/flavours/glitch/components/gifv.tsx @@ -1,6 +1,7 @@ -import React, { useCallback, useState } from 'react'; +import { useCallback, useState } from 'react'; +import * as React from 'react'; -type Props = { +interface Props { src: string; key: string; alt?: string; @@ -8,7 +9,7 @@ type Props = { width: number; height: number; onClick?: () => void; -}; +} export const GIFV: React.FC = ({ src, diff --git a/app/javascript/flavours/glitch/components/hashtag.jsx b/app/javascript/flavours/glitch/components/hashtag.jsx index 235aaeb3c1..422ead01db 100644 --- a/app/javascript/flavours/glitch/components/hashtag.jsx +++ b/app/javascript/flavours/glitch/components/hashtag.jsx @@ -1,15 +1,21 @@ // @ts-check -import React from 'react'; -import { Sparklines, SparklinesCurve } from 'react-sparklines'; -import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Permalink from './permalink'; -import ShortNumber from 'flavours/glitch/components/short_number'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Component } from 'react'; + +import { FormattedMessage } from 'react-intl'; + import classNames from 'classnames'; -class SilentErrorBoundary extends React.Component { +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +import ShortNumber from 'flavours/glitch/components/short_number'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; + +import Permalink from './permalink'; + +class SilentErrorBoundary extends Component { static propTypes = { children: PropTypes.node, @@ -70,7 +76,7 @@ const Hashtag = ({ name, href, to, people, uses, history, className, description
    - {name ? #{name} : } + {name ? <>#{name} : } {description ? ( diff --git a/app/javascript/flavours/glitch/components/icon.tsx b/app/javascript/flavours/glitch/components/icon.tsx index 4eb948dc76..765aa89aec 100644 --- a/app/javascript/flavours/glitch/components/icon.tsx +++ b/app/javascript/flavours/glitch/components/icon.tsx @@ -1,13 +1,14 @@ -import React from 'react'; +import * as React from 'react'; + import classNames from 'classnames'; -type Props = { +interface Props extends React.HTMLAttributes { id: string; className?: string; fixedWidth?: boolean; children?: never; - [key: string]: any; -}; +} + export const Icon: React.FC = ({ id, className, diff --git a/app/javascript/flavours/glitch/components/icon_button.tsx b/app/javascript/flavours/glitch/components/icon_button.tsx index 80b20ae1b0..ecc418773c 100644 --- a/app/javascript/flavours/glitch/components/icon_button.tsx +++ b/app/javascript/flavours/glitch/components/icon_button.tsx @@ -1,9 +1,11 @@ -import React from 'react'; -import classNames from 'classnames'; -import { Icon } from './icon'; -import { AnimatedNumber } from './animated_number'; +import * as React from 'react'; -type Props = { +import classNames from 'classnames'; + +import { AnimatedNumber } from './animated_number'; +import { Icon } from './icon'; + +interface Props { className?: string; title: string; icon: string; @@ -26,11 +28,11 @@ type Props = { obfuscateCount?: boolean; href?: string; ariaHidden: boolean; -}; -type States = { +} +interface States { activate: boolean; deactivate: boolean; -}; +} export class IconButton extends React.PureComponent { static defaultProps = { size: 18, @@ -136,7 +138,7 @@ export class IconButton extends React.PureComponent { } let contents = ( - + <> + ); if (href != null) { diff --git a/app/javascript/flavours/glitch/components/icon_with_badge.tsx b/app/javascript/flavours/glitch/components/icon_with_badge.tsx index bf86814c03..319c0b2389 100644 --- a/app/javascript/flavours/glitch/components/icon_with_badge.tsx +++ b/app/javascript/flavours/glitch/components/icon_with_badge.tsx @@ -1,14 +1,15 @@ -import React from 'react'; +import * as React from 'react'; + import { Icon } from './icon'; const formatNumber = (num: number): number | string => (num > 40 ? '40+' : num); -type Props = { +interface Props { id: string; count: number; issueBadge: boolean; className: string; -}; +} export const IconWithBadge: React.FC = ({ id, count, diff --git a/app/javascript/flavours/glitch/components/inline_account.jsx b/app/javascript/flavours/glitch/components/inline_account.jsx index eeb58b5533..98e44c05e0 100644 --- a/app/javascript/flavours/glitch/components/inline_account.jsx +++ b/app/javascript/flavours/glitch/components/inline_account.jsx @@ -1,8 +1,10 @@ -import React from 'react'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; -import { makeGetAccount } from 'flavours/glitch/selectors'; + import { Avatar } from 'flavours/glitch/components/avatar'; +import { makeGetAccount } from 'flavours/glitch/selectors'; const makeMapStateToProps = () => { const getAccount = makeGetAccount(); @@ -14,7 +16,7 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -class InlineAccount extends React.PureComponent { +class InlineAccount extends PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.jsx b/app/javascript/flavours/glitch/components/intersection_observer_article.jsx index 6c00e557d0..bef40c07fa 100644 --- a/app/javascript/flavours/glitch/components/intersection_observer_article.jsx +++ b/app/javascript/flavours/glitch/components/intersection_observer_article.jsx @@ -1,12 +1,12 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; -import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; +import { cloneElement, Component } from 'react'; +import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; +import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; // Diff these props in the "unrendered" state const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; -export default class IntersectionObserverArticle extends React.Component { +export default class IntersectionObserverArticle extends Component { static propTypes = { intersectionObserverWrapper: PropTypes.object.isRequired, @@ -123,7 +123,7 @@ export default class IntersectionObserverArticle extends React.Component { tabIndex={0} style={style} > - {children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })} + {children && cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })} ); } diff --git a/app/javascript/flavours/glitch/components/link.jsx b/app/javascript/flavours/glitch/components/link.jsx index bbec121a86..9babe7320f 100644 --- a/app/javascript/flavours/glitch/components/link.jsx +++ b/app/javascript/flavours/glitch/components/link.jsx @@ -2,13 +2,13 @@ // ~ 😘 kibi! // Package imports. -import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; +import { PureComponent } from 'react'; + +import classNames from 'classnames'; // Utils. import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; - // Handlers. const handlers = { @@ -25,7 +25,7 @@ const handlers = { }; // The component. -export default class Link extends React.PureComponent { +export default class Link extends PureComponent { // Constructor. constructor (props) { diff --git a/app/javascript/flavours/glitch/components/load_gap.jsx b/app/javascript/flavours/glitch/components/load_gap.jsx deleted file mode 100644 index 9c81df6323..0000000000 --- a/app/javascript/flavours/glitch/components/load_gap.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, defineMessages } from 'react-intl'; -import { Icon } from 'flavours/glitch/components/icon'; - -const messages = defineMessages({ - load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, -}); - -class LoadGap extends React.PureComponent { - - static propTypes = { - disabled: PropTypes.bool, - maxId: PropTypes.string, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleClick = () => { - this.props.onClick(this.props.maxId); - }; - - render () { - const { disabled, intl } = this.props; - - return ( - - ); - } - -} - -export default injectIntl(LoadGap); diff --git a/app/javascript/flavours/glitch/components/load_gap.tsx b/app/javascript/flavours/glitch/components/load_gap.tsx new file mode 100644 index 0000000000..c86e70f9fc --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_gap.tsx @@ -0,0 +1,34 @@ +import { useCallback } from 'react'; + +import { useIntl, defineMessages } from 'react-intl'; + +import { Icon } from 'flavours/glitch/components/icon'; + +const messages = defineMessages({ + load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, +}); + +interface Props { + disabled: boolean; + maxId: string; + onClick: (maxId: string) => void; +} + +export const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { + const intl = useIntl(); + + const handleClick = useCallback(() => { + onClick(maxId); + }, [maxId, onClick]); + + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/load_more.jsx b/app/javascript/flavours/glitch/components/load_more.jsx deleted file mode 100644 index ab9428e35a..0000000000 --- a/app/javascript/flavours/glitch/components/load_more.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class LoadMore extends React.PureComponent { - - static propTypes = { - onClick: PropTypes.func, - disabled: PropTypes.bool, - visible: PropTypes.bool, - }; - - static defaultProps = { - visible: true, - }; - - render() { - const { disabled, visible } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/load_more.tsx b/app/javascript/flavours/glitch/components/load_more.tsx new file mode 100644 index 0000000000..8b5746ad30 --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_more.tsx @@ -0,0 +1,24 @@ +import { FormattedMessage } from 'react-intl'; + +interface Props { + onClick: (event: React.MouseEvent) => void; + disabled?: boolean; + visible?: boolean; +} +export const LoadMore: React.FC = ({ + onClick, + disabled, + visible = true, +}) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/load_pending.jsx b/app/javascript/flavours/glitch/components/load_pending.jsx deleted file mode 100644 index a75259146a..0000000000 --- a/app/javascript/flavours/glitch/components/load_pending.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class LoadPending extends React.PureComponent { - - static propTypes = { - onClick: PropTypes.func, - count: PropTypes.number, - }; - - render() { - const { count } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/load_pending.tsx b/app/javascript/flavours/glitch/components/load_pending.tsx new file mode 100644 index 0000000000..f7589622ed --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_pending.tsx @@ -0,0 +1,18 @@ +import { FormattedMessage } from 'react-intl'; + +interface Props { + onClick: (event: React.MouseEvent) => void; + count: number; +} + +export const LoadPending: React.FC = ({ onClick, count }) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/loading_indicator.jsx b/app/javascript/flavours/glitch/components/loading_indicator.jsx deleted file mode 100644 index 59f721c50f..0000000000 --- a/app/javascript/flavours/glitch/components/loading_indicator.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export const CircularProgress = ({ size, strokeWidth }) => { - const viewBox = `0 0 ${size} ${size}`; - const radius = (size - strokeWidth) / 2; - - return ( - - - - ); -}; - -CircularProgress.propTypes = { - size: PropTypes.number.isRequired, - strokeWidth: PropTypes.number.isRequired, -}; - -const LoadingIndicator = () => ( -
    - -
    -); - -export default LoadingIndicator; diff --git a/app/javascript/flavours/glitch/components/loading_indicator.tsx b/app/javascript/flavours/glitch/components/loading_indicator.tsx new file mode 100644 index 0000000000..6bc24a0d61 --- /dev/null +++ b/app/javascript/flavours/glitch/components/loading_indicator.tsx @@ -0,0 +1,7 @@ +import { CircularProgress } from './circular_progress'; + +export const LoadingIndicator: React.FC = () => ( +
    + +
    +); diff --git a/app/javascript/flavours/glitch/components/logo.jsx b/app/javascript/flavours/glitch/components/logo.jsx index 60e8f40b23..16ca9f80fd 100644 --- a/app/javascript/flavours/glitch/components/logo.jsx +++ b/app/javascript/flavours/glitch/components/logo.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import logo from 'mastodon/../images/logo.svg'; export const WordmarkLogo = () => ( diff --git a/app/javascript/flavours/glitch/components/media_attachments.jsx b/app/javascript/flavours/glitch/components/media_attachments.jsx index b11d3526f9..4e777437a0 100644 --- a/app/javascript/flavours/glitch/components/media_attachments.jsx +++ b/app/javascript/flavours/glitch/components/media_attachments.jsx @@ -1,11 +1,13 @@ -import React from 'react'; import PropTypes from 'prop-types'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video, Audio } from 'flavours/glitch/features/ui/util/async-components'; -import Bundle from 'flavours/glitch/features/ui/components/bundle'; + import noop from 'lodash/noop'; +import Bundle from 'flavours/glitch/features/ui/components/bundle'; +import { MediaGallery, Video, Audio } from 'flavours/glitch/features/ui/util/async-components'; + export default class MediaAttachments extends ImmutablePureComponent { static propTypes = { @@ -50,8 +52,9 @@ export default class MediaAttachments extends ImmutablePureComponent { }; render () { - const { status, lang, width, height, revealed } = this.props; + const { status, width, height, revealed } = this.props; const mediaAttachments = status.get('media_attachments'); + const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang; if (mediaAttachments.size === 0) { return null; @@ -59,14 +62,15 @@ export default class MediaAttachments extends ImmutablePureComponent { if (mediaAttachments.getIn([0, 'type']) === 'audio') { const audio = mediaAttachments.get(0); + const description = audio.getIn(['translation', 'description']) || audio.get('description'); return ( {Component => ( @@ -89,8 +94,8 @@ export default class MediaAttachments extends ImmutablePureComponent { frameRate={video.getIn(['meta', 'original', 'frame_rate'])} blurhash={video.get('blurhash')} src={video.get('url')} - alt={video.get('description')} - lang={lang || status.get('language')} + alt={description} + lang={language} width={width} height={height} inline @@ -107,7 +112,7 @@ export default class MediaAttachments extends ImmutablePureComponent { {Component => ( ALT); } + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); + if (attachment.get('type') === 'unknown') { return (
    - +
    - - - )} - + showTrends ? ( + <> +
    + + + ) : null ); -class NavigationPortal extends React.PureComponent { +class NavigationPortal extends PureComponent { render () { return ( diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx index 53945d6a7a..d0eedc6412 100644 --- a/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx +++ b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import * as React from 'react'; + import { FormattedMessage } from 'react-intl'; export const NotSignedInIndicator: React.FC = () => ( @@ -6,7 +7,7 @@ export const NotSignedInIndicator: React.FC = () => (
    diff --git a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx index 532eb4358c..dfc7ac5a8a 100644 --- a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx +++ b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx @@ -6,13 +6,16 @@ // Package imports // -import React from 'react'; import PropTypes from 'prop-types'; + import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { Icon } from 'flavours/glitch/components/icon'; + import classNames from 'classnames'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Icon } from 'flavours/glitch/components/icon'; + const messages = defineMessages({ btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' }, btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' }, diff --git a/app/javascript/flavours/glitch/components/permalink.jsx b/app/javascript/flavours/glitch/components/permalink.jsx index b09b17eeb4..fa33ce066a 100644 --- a/app/javascript/flavours/glitch/components/permalink.jsx +++ b/app/javascript/flavours/glitch/components/permalink.jsx @@ -1,7 +1,7 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; -export default class Permalink extends React.PureComponent { +export default class Permalink extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -24,9 +24,7 @@ export default class Permalink extends React.PureComponent { if (this.context.router) { e.preventDefault(); - let state = { ...this.context.router.history.location.state }; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(this.props.to, state); + this.context.router.history.push(this.props.to); } } }; diff --git a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx index e8c9611a34..1a290c91de 100644 --- a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx @@ -1,11 +1,14 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { Icon } from 'flavours/glitch/components/icon'; -import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; -import { connect } from 'react-redux'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; -class PictureInPicturePlaceholder extends React.PureComponent { +import { connect } from 'react-redux'; + +import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; +import { Icon } from 'flavours/glitch/components/icon'; + +class PictureInPicturePlaceholder extends PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/components/poll.jsx b/app/javascript/flavours/glitch/components/poll.jsx index df23919563..eca7b5c525 100644 --- a/app/javascript/flavours/glitch/components/poll.jsx +++ b/app/javascript/flavours/glitch/components/poll.jsx @@ -1,15 +1,21 @@ -import React from 'react'; import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; -import Motion from 'flavours/glitch/features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; + import escapeTextContentForBrowser from 'escape-html'; -import emojify from 'flavours/glitch/features/emoji/emoji'; -import { RelativeTimestamp } from './relative_timestamp'; +import spring from 'react-motion/lib/spring'; + import { Icon } from 'flavours/glitch/components/icon'; +import emojify from 'flavours/glitch/features/emoji/emoji'; +import Motion from 'flavours/glitch/features/ui/util/optional_motion'; + +import { RelativeTimestamp } from './relative_timestamp'; + const messages = defineMessages({ closed: { @@ -52,9 +58,9 @@ class Poll extends ImmutablePureComponent { }; static getDerivedStateFromProps (props, state) { - const { poll, intl } = props; + const { poll } = props; const expires_at = poll.get('expires_at'); - const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now(); + const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now(); return (expired === state.expired) ? null : { expired }; } @@ -71,10 +77,10 @@ class Poll extends ImmutablePureComponent { } _setupTimer () { - const { poll, intl } = this.props; + const { poll } = this.props; clearTimeout(this._timer); if (!this.state.expired) { - const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now(); + const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now(); this._timer = setTimeout(() => { this.setState({ expired: true }); }, delay); @@ -133,10 +139,12 @@ class Poll extends ImmutablePureComponent { const active = !!this.state.selected[`${optionIndex}`]; const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); - let titleEmojified = option.get('title_emojified'); - if (!titleEmojified) { + const title = option.getIn(['translation', 'title']) || option.get('title'); + let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml'); + + if (!titleHtml) { const emojiMap = makeEmojiMap(poll); - titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap); + titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap); } return ( @@ -158,7 +166,7 @@ class Poll extends ImmutablePureComponent { role={poll.get('multiple') ? 'checkbox' : 'radio'} onKeyPress={this.handleOptionKeyPress} aria-checked={active} - aria-label={option.get('title')} + aria-label={title} lang={lang} data-index={optionIndex} /> @@ -177,7 +185,7 @@ class Poll extends ImmutablePureComponent { {!!voted && diff --git a/app/javascript/flavours/glitch/components/radio_button.tsx b/app/javascript/flavours/glitch/components/radio_button.tsx index 829f471747..fbd7859d80 100644 --- a/app/javascript/flavours/glitch/components/radio_button.tsx +++ b/app/javascript/flavours/glitch/components/radio_button.tsx @@ -1,13 +1,14 @@ -import React from 'react'; +import * as React from 'react'; + import classNames from 'classnames'; -type Props = { +interface Props { value: string; checked: boolean; name: string; onChange: (event: React.ChangeEvent) => void; label: React.ReactNode; -}; +} export const RadioButton: React.FC = ({ name, diff --git a/app/javascript/flavours/glitch/components/regeneration_indicator.jsx b/app/javascript/flavours/glitch/components/regeneration_indicator.jsx index 68ce09df9b..78844f389d 100644 --- a/app/javascript/flavours/glitch/components/regeneration_indicator.jsx +++ b/app/javascript/flavours/glitch/components/regeneration_indicator.jsx @@ -1,5 +1,5 @@ -import React from 'react'; import { FormattedMessage } from 'react-intl'; + import illustration from 'flavours/glitch/images/elephant_ui_working.svg'; const RegenerationIndicator = () => ( diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.tsx b/app/javascript/flavours/glitch/components/relative_timestamp.tsx index 65d9d27cb2..e4a8437d0e 100644 --- a/app/javascript/flavours/glitch/components/relative_timestamp.tsx +++ b/app/javascript/flavours/glitch/components/relative_timestamp.tsx @@ -1,5 +1,7 @@ -import React from 'react'; -import { injectIntl, defineMessages, InjectedIntl } from 'react-intl'; +import { Component } from 'react'; + +import type { IntlShape } from 'react-intl'; +import { injectIntl, defineMessages } from 'react-intl'; const messages = defineMessages({ today: { id: 'relative_time.today', defaultMessage: 'today' }, @@ -101,7 +103,7 @@ const getUnitDelay = (units: string) => { }; export const timeAgoString = ( - intl: InjectedIntl, + intl: IntlShape, date: Date, now: number, year: number, @@ -153,7 +155,7 @@ export const timeAgoString = ( }; const timeRemainingString = ( - intl: InjectedIntl, + intl: IntlShape, date: Date, now: number, timeGiven = true @@ -187,19 +189,19 @@ const timeRemainingString = ( return relativeTime; }; -type Props = { - intl: InjectedIntl; +interface Props { + intl: IntlShape; timestamp: string; year: number; futureDate?: boolean; short?: boolean; -}; -type States = { +} +interface States { now: number; -}; -class RelativeTimestamp extends React.Component { +} +class RelativeTimestamp extends Component { state = { - now: this.props.intl.now(), + now: Date.now(), }; static defaultProps = { @@ -221,7 +223,7 @@ class RelativeTimestamp extends React.Component { UNSAFE_componentWillReceiveProps(nextProps: Props) { if (this.props.timestamp !== nextProps.timestamp) { - this.setState({ now: this.props.intl.now() }); + this.setState({ now: Date.now() }); } } @@ -251,7 +253,7 @@ class RelativeTimestamp extends React.Component { : Math.max(updateInterval, unitRemainder); this._timer = window.setTimeout(() => { - this.setState({ now: this.props.intl.now() }); + this.setState({ now: Date.now() }); }, delay); } diff --git a/app/javascript/flavours/glitch/components/scrollable_list.jsx b/app/javascript/flavours/glitch/components/scrollable_list.jsx index 13701bcd3e..8d18c2081b 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.jsx +++ b/app/javascript/flavours/glitch/components/scrollable_list.jsx @@ -1,20 +1,28 @@ -import React, { PureComponent } from 'react'; -import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import PropTypes from 'prop-types'; -import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; -import LoadMore from './load_more'; -import LoadPending from './load_pending'; -import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/intersection_observer_wrapper'; -import { throttle } from 'lodash'; -import { List as ImmutableList } from 'immutable'; +import { Children, cloneElement, PureComponent } from 'react'; + import classNames from 'classnames'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; -import LoadingIndicator from './loading_indicator'; + +import { List as ImmutableList } from 'immutable'; import { connect } from 'react-redux'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import { throttle } from 'lodash'; + +import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; +import ScrollContainer from 'flavours/glitch/containers/scroll_container'; +import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/intersection_observer_wrapper'; + +import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; + +import { LoadMore } from './load_more'; +import { LoadPending } from './load_pending'; +import { LoadingIndicator } from './loading_indicator'; + const MOUSE_IDLE_DELAY = 300; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + const mapStateToProps = (state, { scrollKey }) => { return { preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']), @@ -182,8 +190,8 @@ class ScrollableList extends PureComponent { }; getSnapshotBeforeUpdate (prevProps) { - const someItemInserted = React.Children.count(prevProps.children) > 0 && - React.Children.count(prevProps.children) < React.Children.count(this.props.children) && + const someItemInserted = Children.count(prevProps.children) > 0 && + Children.count(prevProps.children) < Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); @@ -237,20 +245,20 @@ class ScrollableList extends PureComponent { attachScrollListener () { if (this.props.bindToDocument) { document.addEventListener('scroll', this.handleScroll); - document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : undefined); + this.node.addEventListener('wheel', this.handleWheel, listenerOptions); } } detachScrollListener () { if (this.props.bindToDocument) { document.removeEventListener('scroll', this.handleScroll); - document.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel); + this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } } @@ -291,7 +299,7 @@ class ScrollableList extends PureComponent { render () { const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; - const childrenCount = React.Children.count(children); + const childrenCount = Children.count(children); const loadMore = (hasMore && onLoadMore) ? : null; const loadPending = (numPending > 0) ? : null; @@ -317,7 +325,7 @@ class ScrollableList extends PureComponent { {loadPending} - {React.Children.map(this.props.children, (child, index) => ( + {Children.map(this.props.children, (child, index) => ( - {React.cloneElement(child, { + {cloneElement(child, { getScrollPosition: this.getScrollPosition, updateScrollBottom: this.updateScrollBottom, cachedMediaWidth: this.state.cachedMediaWidth, diff --git a/app/javascript/flavours/glitch/components/server_banner.jsx b/app/javascript/flavours/glitch/components/server_banner.jsx index 8b9c501538..4809df1ddc 100644 --- a/app/javascript/flavours/glitch/components/server_banner.jsx +++ b/app/javascript/flavours/glitch/components/server_banner.jsx @@ -1,14 +1,18 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PureComponent } from 'react'; + import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; + +import { Link } from 'react-router-dom'; + import { connect } from 'react-redux'; + import { fetchServer } from 'flavours/glitch/actions/server'; +import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; import ShortNumber from 'flavours/glitch/components/short_number'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; import Account from 'flavours/glitch/containers/account_container'; import { domain } from 'flavours/glitch/initial_state'; -import { Image } from 'flavours/glitch/components/image'; -import { Link } from 'react-router-dom'; const messages = defineMessages({ aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' }, @@ -18,7 +22,7 @@ const mapStateToProps = state => ({ server: state.getIn(['server', 'server']), }); -class ServerBanner extends React.PureComponent { +class ServerBanner extends PureComponent { static propTypes = { server: PropTypes.object, @@ -41,7 +45,7 @@ class ServerBanner extends React.PureComponent { {domain}
    , mastodon:
    Mastodon }} />
    - +
    {isLoading ? ( diff --git a/app/javascript/flavours/glitch/components/image.tsx b/app/javascript/flavours/glitch/components/server_hero_image.tsx similarity index 80% rename from app/javascript/flavours/glitch/components/image.tsx rename to app/javascript/flavours/glitch/components/server_hero_image.tsx index 490543424a..be05059e49 100644 --- a/app/javascript/flavours/glitch/components/image.tsx +++ b/app/javascript/flavours/glitch/components/server_hero_image.tsx @@ -1,15 +1,18 @@ -import React, { useCallback, useState } from 'react'; -import { Blurhash } from './blurhash'; +import { useCallback, useState } from 'react'; +import * as React from 'react'; + import classNames from 'classnames'; -type Props = { +import { Blurhash } from './blurhash'; + +interface Props { src: string; srcSet?: string; blurhash?: string; className?: string; -}; +} -export const Image: React.FC = ({ +export const ServerHeroImage: React.FC = ({ src, srcSet, blurhash, diff --git a/app/javascript/flavours/glitch/components/setting_text.jsx b/app/javascript/flavours/glitch/components/setting_text.jsx index 3a21a06018..79d4bf8ea3 100644 --- a/app/javascript/flavours/glitch/components/setting_text.jsx +++ b/app/javascript/flavours/glitch/components/setting_text.jsx @@ -1,8 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; -export default class SettingText extends React.PureComponent { +export default class SettingText extends PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/components/short_number.jsx b/app/javascript/flavours/glitch/components/short_number.jsx index 0c40941c0d..0ddd26e783 100644 --- a/app/javascript/flavours/glitch/components/short_number.jsx +++ b/app/javascript/flavours/glitch/components/short_number.jsx @@ -1,7 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; +import { memo } from 'react'; + import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; // @ts-check /** @@ -109,4 +111,4 @@ ShortNumberCounter.propTypes = { value: PropTypes.arrayOf(PropTypes.number), }; -export default React.memo(ShortNumber); +export default memo(ShortNumber); diff --git a/app/javascript/flavours/glitch/components/skeleton.jsx b/app/javascript/flavours/glitch/components/skeleton.jsx deleted file mode 100644 index 6a17ffb261..0000000000 --- a/app/javascript/flavours/glitch/components/skeleton.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Skeleton = ({ width, height }) => ; - -Skeleton.propTypes = { - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -export default Skeleton; diff --git a/app/javascript/flavours/glitch/components/skeleton.tsx b/app/javascript/flavours/glitch/components/skeleton.tsx new file mode 100644 index 0000000000..30ff0d8525 --- /dev/null +++ b/app/javascript/flavours/glitch/components/skeleton.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; + +interface Props { + width?: number | string; + height?: number | string; +} + +export const Skeleton: React.FC = ({ width, height }) => ( + + ‌ + +); diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 57f244def9..ec1c0db223 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -1,34 +1,43 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import StatusPrepend from './status_prepend'; + +import { injectIntl, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { HotKeys } from 'react-hotkeys'; + +import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; +import PollContainer from 'flavours/glitch/containers/poll_container'; +import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; +import { displayMedia } from 'flavours/glitch/initial_state'; +import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; + +import Card from '../features/status/components/card'; +import Bundle from '../features/ui/components/bundle'; +import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; + +import AttachmentList from './attachment_list'; +import StatusActionBar from './status_action_bar'; +import StatusContent from './status_content'; import StatusHeader from './status_header'; import StatusIcons from './status_icons'; -import StatusContent from './status_content'; -import StatusActionBar from './status_action_bar'; -import AttachmentList from './attachment_list'; -import Card from '../features/status/components/card'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; -import { HotKeys } from 'react-hotkeys'; -import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; -import classNames from 'classnames'; -import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; -import PollContainer from 'flavours/glitch/containers/poll_container'; -import { displayMedia } from 'flavours/glitch/initial_state'; -import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; +import StatusPrepend from './status_prepend'; -// We use the component (and not the container) since we do not want -// to use the progress bar to show download progress -import Bundle from '../features/ui/components/bundle'; +const domParser = new DOMParser(); export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => { const displayName = status.getIn(['account', 'display_name']); + const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text'); + const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml'); + const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent; + const values = [ displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName, - status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length), + spoilerText && !expanded ? spoilerText : contentText, intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), status.getIn(['account', 'acct']), ]; @@ -369,9 +378,7 @@ class Status extends ImmutablePureComponent { status.getIn(['reblog', 'id'], status.get('id')) }`; } - let state = { ...router.history.location.state }; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - router.history.push(destination, state); + router.history.push(destination); } e.preventDefault(); } @@ -391,11 +398,14 @@ class Status extends ImmutablePureComponent { handleOpenVideo = (options) => { const { status } = this.props; - this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); + const lang = status.getIn(['translation', 'language']) || status.get('language'); + this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options); }; handleOpenMedia = (media, index) => { - this.props.onOpenMedia(this.props.status.get('id'), media, index); + const { status } = this.props; + const lang = status.getIn(['translation', 'language']) || status.get('language'); + this.props.onOpenMedia(status.get('id'), media, index, lang); }; handleHotkeyOpenMedia = e => { @@ -405,10 +415,11 @@ class Status extends ImmutablePureComponent { e.preventDefault(); if (status.get('media_attachments').size > 0) { + const lang = status.getIn(['translation', 'language']) || status.get('language'); if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 }); + onOpenVideo(statusId, status.getIn(['media_attachments', 0]), lang, { startTime: 0 }); } else { - onOpenMedia(statusId, status.get('media_attachments'), 0); + onOpenMedia(statusId, status.get('media_attachments'), 0, lang); } } }; @@ -442,16 +453,12 @@ class Status extends ImmutablePureComponent { }; handleHotkeyOpen = () => { - let state = { ...this.context.router.history.location.state }; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; const status = this.props.status; - this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state); + this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); }; handleHotkeyOpenProfile = () => { - let state = { ...this.context.router.history.location.state }; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); }; handleHotkeyMoveUp = e => { @@ -628,6 +635,8 @@ class Status extends ImmutablePureComponent { media.push(); mediaIcons.push('video-camera'); } else if (attachments.size > 0) { + const language = status.getIn(['translation', 'language']) || status.get('language'); + if (muted || attachments.some(item => item.get('type') === 'unknown')) { media.push( {Component => ( @@ -673,8 +684,8 @@ class Status extends ImmutablePureComponent { frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} blurhash={attachment.get('blurhash')} src={attachment.get('url')} - alt={attachment.get('description')} - lang={status.get('language')} + alt={description} + lang={language} inline sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} @@ -694,7 +705,7 @@ class Status extends ImmutablePureComponent { {Component => ( ); + const language = status.getIn(['translation', 'language']) || status.get('language'); + contentMedia.push(); contentMediaIcons.push('tasks'); } diff --git a/app/javascript/flavours/glitch/components/status_action_bar.jsx b/app/javascript/flavours/glitch/components/status_action_bar.jsx index 6f9853afdf..58a793a5cd 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.jsx +++ b/app/javascript/flavours/glitch/components/status_action_bar.jsx @@ -1,21 +1,25 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { IconButton } from './icon_button'; -import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; + import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'flavours/glitch/initial_state'; -import { RelativeTimestamp } from './relative_timestamp'; -import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; + import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; +import { me } from 'flavours/glitch/initial_state'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; +import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; + +import { IconButton } from './icon_button'; +import { RelativeTimestamp } from './relative_timestamp'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, edit: { id: 'status.edit', defaultMessage: 'Edit' }, - direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, + direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, @@ -101,7 +105,6 @@ class StatusActionBar extends ImmutablePureComponent { handleShareClick = () => { navigator.share({ - text: this.props.status.get('search_index'), url: this.props.status.get('url'), }); }; @@ -176,10 +179,9 @@ class StatusActionBar extends ImmutablePureComponent { handleOpen = () => { let state = { ...this.context.router.history.location.state }; if (state.mastodonModalKey) { - this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 }); + this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`); } else { - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, state); + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`); } }; @@ -232,6 +234,10 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); + if (publicStatus && 'share' in navigator) { + menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick }); + } + if (publicStatus) { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); } @@ -250,21 +256,21 @@ class StatusActionBar extends ImmutablePureComponent { if (writtenByMe) { menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true }); } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push(null); if (!this.props.onFilter) { - menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick }); + menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true }); menu.push(null); } - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true }); if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { menu.push(null); @@ -291,10 +297,6 @@ class StatusActionBar extends ImmutablePureComponent { replyTitle = intl.formatMessage(messages.replyAll); } - const shareButton = ('share' in navigator) && publicStatus && ( - - ); - const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; let reblogTitle = ''; @@ -326,7 +328,6 @@ class StatusActionBar extends ImmutablePureComponent { - {shareButton} {filterButton} diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index f1c12e0a09..fc3676dd1f 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -1,14 +1,19 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { FormattedMessage, injectIntl } from 'react-intl'; -import Permalink from './permalink'; -import { connect } from 'react-redux'; + import classnames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; + import { Icon } from 'flavours/glitch/components/icon'; import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; +import Permalink from './permalink'; + const textMatchesTarget = (text, origin, host) => { return (text === origin || text === host || text.startsWith(origin + '/') || text.startsWith(host + '/') @@ -63,7 +68,7 @@ const isLinkMisleading = (link) => { return !(textMatchesTarget(text, origin, host) || textMatchesTarget(text.toLowerCase(), origin, host)); }; -class TranslateButton extends React.PureComponent { +class TranslateButton extends PureComponent { static propTypes = { translation: ImmutablePropTypes.map, @@ -104,7 +109,7 @@ const mapStateToProps = state => ({ languages: state.getIn(['server', 'translationLanguages', 'items']), }); -class StatusContent extends React.PureComponent { +class StatusContent extends PureComponent { static contextTypes = { identity: PropTypes.object, @@ -322,11 +327,11 @@ class StatusContent extends React.PureComponent { const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const contentLocale = intl.locale.replace(/[_-].*/, ''); const targetLanguages = this.props.languages?.get(status.get('language') || 'und'); - const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale); + const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); - const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; - const spoilerContent = { __html: status.get('spoilerHtml') }; - const lang = status.get('translation') ? intl.locale : status.get('language'); + const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') }; + const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') }; + const language = status.getIn(['translation', 'language']) || status.get('language'); const classNames = classnames('status__content', { 'status__content--with-action': parseClick && !disabled, 'status__content--with-spoiler': status.get('spoiler_text').length > 0, @@ -422,7 +427,7 @@ class StatusContent extends React.PureComponent {
    @@ -117,7 +123,7 @@ class Results extends React.PureComponent { {intl.formatMessage(messages.title, { q })} - + ); } diff --git a/app/javascript/flavours/glitch/features/explore/statuses.jsx b/app/javascript/flavours/glitch/features/explore/statuses.jsx index 381c50c5d5..ce484ef77c 100644 --- a/app/javascript/flavours/glitch/features/explore/statuses.jsx +++ b/app/javascript/flavours/glitch/features/explore/statuses.jsx @@ -1,20 +1,25 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import StatusList from 'flavours/glitch/components/status_list'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; -import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends'; + import { debounce } from 'lodash'; + +import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends'; import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import StatusList from 'flavours/glitch/components/status_list'; +import { getStatusList } from 'flavours/glitch/selectors'; const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'trending', 'items']), + statusIds: getStatusList(state, 'trending'), isLoading: state.getIn(['status_lists', 'trending', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'trending', 'next']), }); -class Statuses extends React.PureComponent { +class Statuses extends PureComponent { static propTypes = { statusIds: ImmutablePropTypes.list, @@ -42,7 +47,7 @@ class Statuses extends React.PureComponent { return ( <> - + ({ suggestions: state.getIn(['suggestions', 'items']), isLoading: state.getIn(['suggestions', 'isLoading']), }); -class Suggestions extends React.PureComponent { +class Suggestions extends PureComponent { static propTypes = { isLoading: PropTypes.bool, diff --git a/app/javascript/flavours/glitch/features/explore/tags.jsx b/app/javascript/flavours/glitch/features/explore/tags.jsx index e0fdd1d912..e3a8a45fdf 100644 --- a/app/javascript/flavours/glitch/features/explore/tags.jsx +++ b/app/javascript/flavours/glitch/features/explore/tags.jsx @@ -1,19 +1,24 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import { connect } from 'react-redux'; -import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; + +import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends'; import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; + + const mapStateToProps = state => ({ hashtags: state.getIn(['trends', 'tags', 'items']), isLoadingHashtags: state.getIn(['trends', 'tags', 'isLoading']), }); -class Tags extends React.PureComponent { +class Tags extends PureComponent { static propTypes = { hashtags: ImmutablePropTypes.list, @@ -31,7 +36,7 @@ class Tags extends React.PureComponent { const banner = ( - + ); diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx b/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx index 60d281f974..ed11f2b8ce 100644 --- a/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.jsx @@ -1,23 +1,28 @@ -import { debounce } from 'lodash'; import PropTypes from 'prop-types'; -import React from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import { Helmet } from 'react-helmet'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; + +import { debounce } from 'lodash'; + import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { fetchFavouritedStatuses, expandFavouritedStatuses } from 'flavours/glitch/actions/favourites'; import ColumnHeader from 'flavours/glitch/components/column_header'; import StatusList from 'flavours/glitch/components/status_list'; import Column from 'flavours/glitch/features/ui/components/column'; +import { getStatusList } from 'flavours/glitch/selectors'; const messages = defineMessages({ heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'favourites', 'items']), + statusIds: getStatusList(state, 'favourites'), isLoading: state.getIn(['status_lists', 'favourites', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'favourites', 'next']), }); @@ -34,7 +39,7 @@ class Favourites extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchFavouritedStatuses()); } diff --git a/app/javascript/flavours/glitch/features/favourites/index.jsx b/app/javascript/flavours/glitch/features/favourites/index.jsx index 8755811d94..a5f21e577e 100644 --- a/app/javascript/flavours/glitch/features/favourites/index.jsx +++ b/app/javascript/flavours/glitch/features/favourites/index.jsx @@ -1,17 +1,21 @@ import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; + +import { fetchFavourites } from 'flavours/glitch/actions/interactions'; import ColumnHeader from 'flavours/glitch/components/column_header'; import { Icon } from 'flavours/glitch/components/icon'; -import { fetchFavourites } from 'flavours/glitch/actions/interactions'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; -import { Helmet } from 'react-helmet'; + const messages = defineMessages({ heading: { id: 'column.favourited_by', defaultMessage: 'Favourited by' }, @@ -32,13 +36,13 @@ class Favourites extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - componentWillMount () { + UNSAFE_componentWillMount () { if (!this.props.accountIds) { this.props.dispatch(fetchFavourites(this.props.params.statusId)); } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this.props.dispatch(fetchFavourites(nextProps.params.statusId)); } diff --git a/app/javascript/flavours/glitch/features/filters/added_to_filter.jsx b/app/javascript/flavours/glitch/features/filters/added_to_filter.jsx index 2f3f98c813..743d379474 100644 --- a/app/javascript/flavours/glitch/features/filters/added_to_filter.jsx +++ b/app/javascript/flavours/glitch/features/filters/added_to_filter.jsx @@ -1,16 +1,19 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; -import { toServerSideType } from 'flavours/glitch/utils/filters'; -import Button from 'flavours/glitch/components/button'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; +import Button from 'flavours/glitch/components/button'; +import { toServerSideType } from 'flavours/glitch/utils/filters'; + const mapStateToProps = (state, { filterId }) => ({ filter: state.getIn(['filters', filterId]), }); -class AddedToFilter extends React.PureComponent { +class AddedToFilter extends PureComponent { static propTypes = { onClose: PropTypes.func.isRequired, @@ -30,7 +33,7 @@ class AddedToFilter extends React.PureComponent { let expiredMessage = null; if (filter.get('expires_at') && filter.get('expires_at') < new Date()) { expiredMessage = ( - + <>

    -
    + ); } let contextMismatchMessage = null; if (contextType && !filter.get('context').includes(toServerSideType(contextType))) { contextMismatchMessage = ( - + <>

    -
    + ); } @@ -67,7 +70,7 @@ class AddedToFilter extends React.PureComponent { ); return ( - + <>

    - + ); } diff --git a/app/javascript/flavours/glitch/features/filters/select_filter.jsx b/app/javascript/flavours/glitch/features/filters/select_filter.jsx index e7cdd4e197..bae1d9ef95 100644 --- a/app/javascript/flavours/glitch/features/filters/select_filter.jsx +++ b/app/javascript/flavours/glitch/features/filters/select_filter.jsx @@ -1,11 +1,15 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { connect } from 'react-redux'; + +import fuzzysort from 'fuzzysort'; + +import { Icon } from 'flavours/glitch/components/icon'; import { toServerSideType } from 'flavours/glitch/utils/filters'; import { loupeIcon, deleteIcon } from 'flavours/glitch/utils/icons'; -import { Icon } from 'flavours/glitch/components/icon'; -import fuzzysort from 'fuzzysort'; const messages = defineMessages({ search: { id: 'filter_modal.select_filter.search', defaultMessage: 'Search or create' }, @@ -22,7 +26,7 @@ const mapStateToProps = (state, { contextType }) => ({ ]), }); -class SelectFilter extends React.PureComponent { +class SelectFilter extends PureComponent { static propTypes = { onSelectFilter: PropTypes.func.isRequired, @@ -169,7 +173,7 @@ class SelectFilter extends React.PureComponent { const results = this.search(); return ( - + <>

    @@ -183,7 +187,7 @@ class SelectFilter extends React.PureComponent { {isSearching && this.renderCreateNew(searchValue) }
    - + ); } diff --git a/app/javascript/flavours/glitch/features/firehose/index.jsx b/app/javascript/flavours/glitch/features/firehose/index.jsx new file mode 100644 index 0000000000..ae71ba1764 --- /dev/null +++ b/app/javascript/flavours/glitch/features/firehose/index.jsx @@ -0,0 +1,230 @@ +import PropTypes from 'prop-types'; +import { useRef, useCallback, useEffect } from 'react'; + +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; +import { NavLink } from 'react-router-dom'; + +import { addColumn } from 'flavours/glitch/actions/columns'; +import { changeSetting } from 'flavours/glitch/actions/settings'; +import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming'; +import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import SettingText from 'flavours/glitch/components/setting_text'; +import initialState, { domain } from 'flavours/glitch/initial_state'; +import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; + +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import SettingToggle from '../notifications/components/setting_toggle'; +import StatusListContainer from '../ui/containers/status_list_container'; + +const messages = defineMessages({ + title: { id: 'column.firehose', defaultMessage: 'Live feeds' }, + filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, +}); + +// TODO: use a proper React context later on +const useIdentity = () => ({ + signedIn: !!initialState.meta.me, + accountId: initialState.meta.me, + disabledAccountId: initialState.meta.disabled_account_id, + accessToken: initialState.meta.access_token, + permissions: initialState.role ? initialState.role.permissions : 0, +}); + +const ColumnSettings = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const settings = useAppSelector((state) => state.getIn(['settings', 'firehose'])); + const onChange = useCallback( + (key, checked) => dispatch(changeSetting(['firehose', ...key], checked)), + [dispatch], + ); + + return ( +
    +
    + } + /> + } + /> + + +
    +
    + ); +}; + +const Firehose = ({ feedType, multiColumn }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const { signedIn } = useIdentity(); + const columnRef = useRef(null); + + const onlyMedia = useAppSelector((state) => state.getIn(['settings', 'firehose', 'onlyMedia'], false)); + const hasUnread = useAppSelector((state) => state.getIn(['timelines', `${feedType}${onlyMedia ? ':media' : ''}`, 'unread'], 0) > 0); + + const allowLocalOnly = useAppSelector((state) => state.getIn(['settings', 'firehose', 'allowLocalOnly'])); + const regex = useAppSelector((state) => state.getIn(['settings', 'firehose', 'regex', 'body'])); + + const handlePin = useCallback( + () => { + switch(feedType) { + case 'community': + dispatch(addColumn('COMMUNITY', { other: { onlyMedia }, regex: { body: regex } })); + break; + case 'public': + dispatch(addColumn('PUBLIC', { other: { onlyMedia, allowLocalOnly }, regex: { body: regex } })); + break; + case 'public:remote': + dispatch(addColumn('REMOTE', { other: { onlyMedia, onlyRemote: true }, regex: { body: regex } })); + break; + } + }, + [dispatch, onlyMedia, feedType, allowLocalOnly, regex], + ); + + const handleLoadMore = useCallback( + (maxId) => { + switch(feedType) { + case 'community': + dispatch(expandCommunityTimeline({ onlyMedia })); + break; + case 'public': + dispatch(expandPublicTimeline({ maxId, onlyMedia, allowLocalOnly })); + break; + case 'public:remote': + dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote: true })); + break; + } + }, + [dispatch, onlyMedia, allowLocalOnly, feedType], + ); + + const handleHeaderClick = useCallback(() => columnRef.current?.scrollTop(), []); + + useEffect(() => { + let disconnect; + + switch(feedType) { + case 'community': + dispatch(expandCommunityTimeline({ onlyMedia })); + if (signedIn) { + disconnect = dispatch(connectCommunityStream({ onlyMedia })); + } + break; + case 'public': + dispatch(expandPublicTimeline({ onlyMedia })); + if (signedIn) { + disconnect = dispatch(connectPublicStream({ onlyMedia, allowLocalOnly })); + } + break; + case 'public:remote': + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote: true })); + if (signedIn) { + disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote: true })); + } + break; + } + + return () => disconnect?.(); + }, [dispatch, signedIn, feedType, onlyMedia, allowLocalOnly]); + + const prependBanner = feedType === 'community' ? ( + + + + ) : ( + + + + ); + + const emptyMessage = feedType === 'community' ? ( + + ) : ( + + ); + + return ( + + + + + +
    +
    + + + + + + + + + + + +
    + + +
    + + + {intl.formatMessage(messages.title)} + + +
    + ); +} + +Firehose.propTypes = { + multiColumn: PropTypes.bool, + feedType: PropTypes.string, +}; + +export default Firehose; diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx index e15bc14222..89a270d092 100644 --- a/app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx +++ b/app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx @@ -1,15 +1,17 @@ -import React from 'react'; import PropTypes from 'prop-types'; + +import { injectIntl, defineMessages } from 'react-intl'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import { makeGetAccount } from 'flavours/glitch/selectors'; -import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; -import Permalink from 'flavours/glitch/components/permalink'; -import { IconButton } from 'flavours/glitch/components/icon_button'; -import { injectIntl, defineMessages } from 'react-intl'; + import { followAccount, unfollowAccount } from 'flavours/glitch/actions/accounts'; +import { Avatar } from 'flavours/glitch/components/avatar'; +import { DisplayName } from 'flavours/glitch/components/display_name'; +import { IconButton } from 'flavours/glitch/components/icon_button'; +import Permalink from 'flavours/glitch/components/permalink'; +import { makeGetAccount } from 'flavours/glitch/selectors'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, diff --git a/app/javascript/flavours/glitch/features/follow_recommendations/index.jsx b/app/javascript/flavours/glitch/features/follow_recommendations/index.jsx index 70f2191f11..194784a7a5 100644 --- a/app/javascript/flavours/glitch/features/follow_recommendations/index.jsx +++ b/app/javascript/flavours/glitch/features/follow_recommendations/index.jsx @@ -1,19 +1,23 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; + import { FormattedMessage } from 'react-intl'; -import { fetchSuggestions } from 'flavours/glitch/actions/suggestions'; -import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings'; -import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'; -import { markAsPartial } from 'flavours/glitch/actions/timelines'; -import Column from 'flavours/glitch/features/ui/components/column'; -import Account from './components/account'; -import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg'; -import Button from 'flavours/glitch/components/button'; + import { Helmet } from 'react-helmet'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'; +import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings'; +import { fetchSuggestions } from 'flavours/glitch/actions/suggestions'; +import { markAsPartial } from 'flavours/glitch/actions/timelines'; +import Button from 'flavours/glitch/components/button'; +import Column from 'flavours/glitch/features/ui/components/column'; +import imageGreeting from 'mastodon/../images/elephant_ui_greeting.svg'; + +import Account from './components/account'; + const mapStateToProps = state => ({ suggestions: state.getIn(['suggestions', 'items']), isLoading: state.getIn(['suggestions', 'isLoading']), @@ -86,7 +90,7 @@ class FollowRecommendations extends ImmutablePureComponent {
    {!isLoading && ( - + <>
    {suggestions.size > 0 ? suggestions.map(suggestion => ( @@ -101,7 +105,7 @@ class FollowRecommendations extends ImmutablePureComponent {
    -
    + )}
    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 b2dfc5dbb3..1f665c0496 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 @@ -1,13 +1,15 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Permalink from 'flavours/glitch/components/permalink'; -import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; -import { IconButton } from 'flavours/glitch/components/icon_button'; + import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Avatar } from 'flavours/glitch/components/avatar'; +import { DisplayName } from 'flavours/glitch/components/display_name'; +import { IconButton } from 'flavours/glitch/components/icon_button'; +import Permalink from 'flavours/glitch/components/permalink'; + const messages = defineMessages({ authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, diff --git a/app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js b/app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js index 693e98e8ce..854ebaecb8 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js +++ b/app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js @@ -1,7 +1,9 @@ import { connect } from 'react-redux'; -import { makeGetAccount } from 'flavours/glitch/selectors'; -import AccountAuthorize from '../components/account_authorize'; + import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts'; +import { makeGetAccount } from 'flavours/glitch/selectors'; + +import AccountAuthorize from '../components/account_authorize'; const makeMapStateToProps = () => { const getAccount = makeGetAccount(); diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.jsx b/app/javascript/flavours/glitch/features/follow_requests/index.jsx index a9a35f54b8..70a2ac1e42 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.jsx +++ b/app/javascript/flavours/glitch/features/follow_requests/index.jsx @@ -1,18 +1,28 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { debounce } from 'lodash'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; -import AccountAuthorizeContainer from './containers/account_authorize_container'; -import { fetchFollowRequests, expandFollowRequests } from 'flavours/glitch/actions/accounts'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ScrollableList from 'flavours/glitch/components/scrollable_list'; -import { me } from 'flavours/glitch/initial_state'; + import { Helmet } from 'react-helmet'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { debounce } from 'lodash'; + +import { fetchFollowRequests, expandFollowRequests } from 'flavours/glitch/actions/accounts'; +import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import Column from 'flavours/glitch/features/ui/components/column'; +import { me } from 'flavours/glitch/initial_state'; + +import AccountAuthorizeContainer from './containers/account_authorize_container'; + + + + + + const messages = defineMessages({ heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, }); @@ -39,7 +49,7 @@ class FollowRequests extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchFollowRequests()); } diff --git a/app/javascript/flavours/glitch/features/followed_tags/index.jsx b/app/javascript/flavours/glitch/features/followed_tags/index.jsx index a5abb151f3..9acb77c25f 100644 --- a/app/javascript/flavours/glitch/features/followed_tags/index.jsx +++ b/app/javascript/flavours/glitch/features/followed_tags/index.jsx @@ -1,16 +1,20 @@ -import { debounce } from 'lodash'; import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; + +import { debounce } from 'lodash'; + +import { expandFollowedHashtags, fetchFollowedHashtags } from 'flavours/glitch/actions/tags'; import ColumnHeader from 'flavours/glitch/components/column_header'; +import Hashtag from 'flavours/glitch/components/hashtag'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import Column from 'flavours/glitch/features/ui/components/column'; -import { Helmet } from 'react-helmet'; -import Hashtag from 'flavours/glitch/components/hashtag'; -import { expandFollowedHashtags, fetchFollowedHashtags } from 'flavours/glitch/actions/tags'; const messages = defineMessages({ heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' }, diff --git a/app/javascript/flavours/glitch/features/followers/index.jsx b/app/javascript/flavours/glitch/features/followers/index.jsx index 2c4db665af..935a0223e5 100644 --- a/app/javascript/flavours/glitch/features/followers/index.jsx +++ b/app/javascript/flavours/glitch/features/followers/index.jsx @@ -1,27 +1,31 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; + +import { FormattedMessage } from 'react-intl'; + import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + import { debounce } from 'lodash'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; + import { lookupAccount, fetchAccount, fetchFollowers, expandFollowers, } from 'flavours/glitch/actions/accounts'; -import { FormattedMessage } from 'react-intl'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import { TimelineHint } from 'flavours/glitch/components/timeline_hint'; import AccountContainer from 'flavours/glitch/containers/account_container'; -import Column from 'flavours/glitch/features/ui/components/column'; import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ScrollableList from 'flavours/glitch/components/scrollable_list'; -import TimelineHint from 'flavours/glitch/components/timeline_hint'; -import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; -import { getAccountHidden } from 'flavours/glitch/selectors'; -import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; +import Column from 'flavours/glitch/features/ui/components/column'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import { getAccountHidden } from 'flavours/glitch/selectors'; + +import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; const mapStateToProps = (state, { params: { acct, id } }) => { const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); diff --git a/app/javascript/flavours/glitch/features/following/index.jsx b/app/javascript/flavours/glitch/features/following/index.jsx index 39ff4c6032..498aa4fbe5 100644 --- a/app/javascript/flavours/glitch/features/following/index.jsx +++ b/app/javascript/flavours/glitch/features/following/index.jsx @@ -1,27 +1,31 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; + +import { FormattedMessage } from 'react-intl'; + import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + import { debounce } from 'lodash'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; + import { lookupAccount, fetchAccount, fetchFollowing, expandFollowing, } from 'flavours/glitch/actions/accounts'; -import { FormattedMessage } from 'react-intl'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import { TimelineHint } from 'flavours/glitch/components/timeline_hint'; import AccountContainer from 'flavours/glitch/containers/account_container'; -import Column from 'flavours/glitch/features/ui/components/column'; import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ScrollableList from 'flavours/glitch/components/scrollable_list'; -import TimelineHint from 'flavours/glitch/components/timeline_hint'; -import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; -import { getAccountHidden } from 'flavours/glitch/selectors'; -import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; +import Column from 'flavours/glitch/features/ui/components/column'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import { getAccountHidden } from 'flavours/glitch/selectors'; + +import LimitedAccountHint from '../account_timeline/components/limited_account_hint'; const mapStateToProps = (state, { params: { acct, id } }) => { const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); diff --git a/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx b/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx index fd76f9773d..fd17f8c9d7 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/components/announcements.jsx @@ -1,20 +1,27 @@ -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ReactSwipeableViews from 'react-swipeable-views'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { IconButton } from 'flavours/glitch/components/icon_button'; -import { Icon } from 'flavours/glitch/components/icon'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl'; -import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'flavours/glitch/initial_state'; -import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg'; -import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; + import classNames from 'classnames'; -import EmojiPickerDropdown from 'flavours/glitch/features/compose/containers/emoji_picker_dropdown_container'; -import { AnimatedNumber } from 'flavours/glitch/components/animated_number'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; +import ReactSwipeableViews from 'react-swipeable-views'; + +import { AnimatedNumber } from 'flavours/glitch/components/animated_number'; +import { Icon } from 'flavours/glitch/components/icon'; +import { IconButton } from 'flavours/glitch/components/icon_button'; +import EmojiPickerDropdown from 'flavours/glitch/features/compose/containers/emoji_picker_dropdown_container'; +import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; +import { autoPlayGif, reduceMotion, disableSwiping, mascot } from 'flavours/glitch/initial_state'; import { assetHost } from 'flavours/glitch/utils/config'; +import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg'; + + const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -148,7 +155,7 @@ class Content extends ImmutablePureComponent { } -class Emoji extends React.PureComponent { +class Emoji extends PureComponent { static propTypes = { emoji: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/getting_started/components/trends.jsx b/app/javascript/flavours/glitch/features/getting_started/components/trends.jsx index d7e222d716..c5bb92b3be 100644 --- a/app/javascript/flavours/glitch/features/getting_started/components/trends.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/components/trends.jsx @@ -1,11 +1,14 @@ -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; + import { FormattedMessage } from 'react-intl'; + import { Link } from 'react-router-dom'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; + export default class Trends extends ImmutablePureComponent { static defaultProps = { diff --git a/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js b/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js index d472323f8f..893da21df9 100644 --- a/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js +++ b/app/javascript/flavours/glitch/features/getting_started/containers/announcements_container.js @@ -1,8 +1,10 @@ -import { connect } from 'react-redux'; -import { addReaction, removeReaction, dismissAnnouncement } from 'flavours/glitch/actions/announcements'; -import Announcements from '../components/announcements'; -import { createSelector } from 'reselect'; import { Map as ImmutableMap } from 'immutable'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { addReaction, removeReaction, dismissAnnouncement } from 'flavours/glitch/actions/announcements'; + +import Announcements from '../components/announcements'; const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap())); diff --git a/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js b/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js index d88dbbaf40..cd08b61d40 100644 --- a/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js +++ b/app/javascript/flavours/glitch/features/getting_started/containers/trends_container.js @@ -1,5 +1,7 @@ import { connect } from 'react-redux'; + import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends'; + import Trends from '../components/trends'; const mapStateToProps = state => ({ diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx index 2ef007da5b..1b3aec5520 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx @@ -1,23 +1,29 @@ -import React from 'react'; +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { List as ImmutableList } from 'immutable'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; +import { fetchLists } from 'flavours/glitch/actions/lists'; +import { openModal } from 'flavours/glitch/actions/modal'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading'; -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; -import { openModal } from 'flavours/glitch/actions/modal'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me, showTrends } from 'flavours/glitch/initial_state'; -import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; -import { List as ImmutableList } from 'immutable'; -import { createSelector } from 'reselect'; -import { fetchLists } from 'flavours/glitch/actions/lists'; -import { preferencesLink } from 'flavours/glitch/utils/backend_links'; -import NavigationBar from '../compose/components/navigation_bar'; import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; +import { me, showTrends } from 'flavours/glitch/initial_state'; +import { preferencesLink } from 'flavours/glitch/utils/backend_links'; + +import NavigationBar from '../compose/components/navigation_bar'; + import TrendsContainer from './containers/trends_container'; -import { Helmet } from 'react-helmet'; + const messages = defineMessages({ heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -28,7 +34,7 @@ const messages = defineMessages({ settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, explore: { id: 'navigation_bar.explore', defaultMessage: 'Explore' }, - direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, + direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, @@ -63,7 +69,10 @@ const makeMapStateToProps = () => { const mapDispatchToProps = dispatch => ({ fetchFollowRequests: () => dispatch(fetchFollowRequests()), fetchLists: () => dispatch(fetchLists()), - openSettings: () => dispatch(openModal('SETTINGS', {})), + openSettings: () => dispatch(openModal({ + modalType: 'SETTINGS', + modalProps: {}, + })), }); const badgeDisplay = (number, limit) => { @@ -96,7 +105,7 @@ class GettingStarted extends ImmutablePureComponent { openSettings: PropTypes.func.isRequired, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.fetchLists(); } @@ -173,13 +182,13 @@ class GettingStarted extends ImmutablePureComponent { {multiColumn && } {navItems} {signedIn && ( - + <> {listItems} { preferencesLink !== undefined && } - + )} diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx b/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx index ad40edd75e..20dfe80b47 100644 --- a/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.jsx @@ -1,20 +1,23 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; + import { defineMessages, injectIntl } from 'react-intl'; + import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { openModal } from 'flavours/glitch/actions/modal'; +import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; +import Column from 'flavours/glitch/features/ui/components/column'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading'; -import { openModal } from 'flavours/glitch/actions/modal'; -import { connect } from 'react-redux'; + const messages = defineMessages({ heading: { id: 'column.heading', defaultMessage: 'Misc' }, subheading: { id: 'column.subheading', defaultMessage: 'Miscellaneous options' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, @@ -35,11 +38,15 @@ class GettingStartedMisc extends ImmutablePureComponent { }; openOnboardingModal = () => { - this.props.dispatch(openModal('ONBOARDING')); + this.props.dispatch(openModal({ + modalType: 'ONBOARDING', + })); }; openFeaturedAccountsModal = () => { - this.props.dispatch(openModal('PINNED_ACCOUNTS_EDITOR')); + this.props.dispatch(openModal({ + modalType: 'PINNED_ACCOUNTS_EDITOR', + })); }; render () { diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.jsx index f140f2d013..c60de4c518 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/components/column_settings.jsx @@ -1,10 +1,14 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Toggle from 'react-toggle'; -import AsyncSelect from 'react-select/async'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + import { NonceProvider } from 'react-select'; +import AsyncSelect from 'react-select/async'; +import Toggle from 'react-toggle'; + import SettingToggle from '../../notifications/components/setting_toggle'; const messages = defineMessages({ @@ -12,7 +16,7 @@ const messages = defineMessages({ noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' }, }); -class ColumnSettings extends React.PureComponent { +class ColumnSettings extends PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js index 004856b048..6e67d1fce4 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/containers/column_settings_container.js @@ -1,8 +1,10 @@ import { connect } from 'react-redux'; -import ColumnSettings from '../components/column_settings'; + import { changeColumnParams } from 'flavours/glitch/actions/columns'; import api from 'flavours/glitch/api'; +import ColumnSettings from '../components/column_settings'; + const mapStateToProps = (state, { columnId }) => { const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === columnId); diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx b/app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx index cd3639d968..de67b06ca8 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.jsx @@ -1,21 +1,28 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; -import Column from 'flavours/glitch/components/column'; -import ColumnHeader from 'flavours/glitch/components/column_header'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; -import { connectHashtagStream } from 'flavours/glitch/actions/streaming'; +import { PureComponent } from 'react'; + import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; -import { isEqual } from 'lodash'; -import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags'; -import { Icon } from 'flavours/glitch/components/icon'; + import classNames from 'classnames'; import { Helmet } from 'react-helmet'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; + +import { isEqual } from 'lodash'; + +import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import { connectHashtagStream } from 'flavours/glitch/actions/streaming'; +import { fetchHashtag, followHashtag, unfollowHashtag } from 'flavours/glitch/actions/tags'; +import { expandHashtagTimeline, clearTimeline } from 'flavours/glitch/actions/timelines'; +import Column from 'flavours/glitch/components/column'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import { Icon } from 'flavours/glitch/components/icon'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; + +import ColumnSettingsContainer from './containers/column_settings_container'; + + const messages = defineMessages({ followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' }, @@ -26,7 +33,7 @@ const mapStateToProps = (state, props) => ({ tag: state.getIn(['tags', props.params.id]), }); -class HashtagTimeline extends React.PureComponent { +class HashtagTimeline extends PureComponent { disconnects = []; diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.jsx index 1eeeaa378a..52b3ad0604 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.jsx @@ -1,16 +1,19 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + import SettingText from 'flavours/glitch/components/setting_text'; +import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle'; const messages = defineMessages({ filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -class ColumnSettings extends React.PureComponent { +class ColumnSettings extends PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, @@ -34,7 +37,7 @@ class ColumnSettings extends React.PureComponent {
    - } /> + } />
    diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/explore_prompt.jsx b/app/javascript/flavours/glitch/features/home_timeline/components/explore_prompt.jsx new file mode 100644 index 0000000000..3eb28c59a1 --- /dev/null +++ b/app/javascript/flavours/glitch/features/home_timeline/components/explore_prompt.jsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import background from 'mastodon/../images/friends-cropped.png'; + + +export const ExplorePrompt = () => ( + + + +

    +

    + +
    +
    + + +
    +
    +
    +); diff --git a/app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js index 16747151b0..215b8c42f0 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js @@ -1,7 +1,9 @@ import { connect } from 'react-redux'; -import ColumnSettings from '../components/column_settings'; + import { changeSetting, saveSettings } from 'flavours/glitch/actions/settings'; +import ColumnSettings from '../components/column_settings'; + const mapStateToProps = state => ({ settings: state.getIn(['settings', 'home']), }); diff --git a/app/javascript/flavours/glitch/features/home_timeline/index.jsx b/app/javascript/flavours/glitch/features/home_timeline/index.jsx index a8dfa1d08f..df25e86489 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/home_timeline/index.jsx @@ -1,20 +1,29 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; import PropTypes from 'prop-types'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; -import Column from 'flavours/glitch/components/column'; -import ColumnHeader from 'flavours/glitch/components/column_header'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { Link } from 'react-router-dom'; -import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements'; -import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container'; + import classNames from 'classnames'; +import { Helmet } from 'react-helmet'; + +import { List as ImmutableList } from 'immutable'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements'; import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge'; import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator'; -import { Helmet } from 'react-helmet'; +import AnnouncementsContainer from 'flavours/glitch/features/getting_started/containers/announcements_container'; +import { me } from 'flavours/glitch/initial_state'; + +import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; +import { expandHomeTimeline } from '../../actions/timelines'; +import Column from '../../components/column'; +import ColumnHeader from '../../components/column_header'; +import StatusListContainer from '../ui/containers/status_list_container'; + +import { ExplorePrompt } from './components/explore_prompt'; +import ColumnSettingsContainer from './containers/column_settings_container'; const messages = defineMessages({ title: { id: 'column.home', defaultMessage: 'Home' }, @@ -22,16 +31,44 @@ const messages = defineMessages({ hide_announcements: { id: 'home.hide_announcements', defaultMessage: 'Hide announcements' }, }); +const getHomeFeedSpeed = createSelector([ + state => state.getIn(['timelines', 'home', 'items'], ImmutableList()), + state => state.getIn(['timelines', 'home', 'pendingItems'], ImmutableList()), + state => state.get('statuses'), +], (statusIds, pendingStatusIds, statusMap) => { + const recentStatusIds = pendingStatusIds.size > 0 ? pendingStatusIds : statusIds; + const statuses = recentStatusIds.filter(id => id !== null).map(id => statusMap.get(id)).filter(status => status?.get('account') !== me).take(20); + const oldest = new Date(statuses.getIn([statuses.size - 1, 'created_at'], 0)); + const newest = new Date(statuses.getIn([0, 'created_at'], 0)); + const averageGap = (newest - oldest) / (1000 * (statuses.size + 1)); // Average gap between posts on first page in seconds + + return { + gap: averageGap, + newest, + }; +}); + +const homeTooSlow = createSelector([ + state => state.getIn(['timelines', 'home', 'isLoading']), + state => state.getIn(['timelines', 'home', 'isPartial']), + getHomeFeedSpeed, +], (isLoading, isPartial, speed) => + !isLoading && !isPartial // Only if the home feed has finished loading + && (speed.gap > (30 * 60) // If the average gap between posts is more than 20 minutes + || (Date.now() - speed.newest) > (1000 * 3600)) // If the most recent post is from over an hour ago +); + const mapStateToProps = state => ({ hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0, isPartial: state.getIn(['timelines', 'home', 'isPartial']), hasAnnouncements: !state.getIn(['announcements', 'items']).isEmpty(), unreadAnnouncements: state.getIn(['announcements', 'items']).count(item => !item.get('read')), showAnnouncements: state.getIn(['announcements', 'show']), + tooSlow: homeTooSlow(state), regex: state.getIn(['settings', 'home', 'regex', 'body']), }); -class HomeTimeline extends React.PureComponent { +class HomeTimeline extends PureComponent { static contextTypes = { identity: PropTypes.object, @@ -47,6 +84,7 @@ class HomeTimeline extends React.PureComponent { hasAnnouncements: PropTypes.bool, unreadAnnouncements: PropTypes.number, showAnnouncements: PropTypes.bool, + tooSlow: PropTypes.bool, regex: PropTypes.string, }; @@ -117,11 +155,11 @@ class HomeTimeline extends React.PureComponent { }; render () { - const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; + const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; const { signedIn } = this.context.identity; - let announcementsButton = null; + let announcementsButton, banner; if (hasAnnouncements) { announcementsButton = ( @@ -136,6 +174,10 @@ class HomeTimeline extends React.PureComponent { ); } + if (tooSlow) { + banner = ; + } + return ( }} />} + emptyMessage={} bindToDocument={!multiColumn} regex={this.props.regex} /> diff --git a/app/javascript/flavours/glitch/features/interaction_modal/index.jsx b/app/javascript/flavours/glitch/features/interaction_modal/index.jsx index 380edee659..c9bbecf1af 100644 --- a/app/javascript/flavours/glitch/features/interaction_modal/index.jsx +++ b/app/javascript/flavours/glitch/features/interaction_modal/index.jsx @@ -1,24 +1,32 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; -import { registrationsOpen } from 'flavours/glitch/initial_state'; -import { connect } from 'react-redux'; -import { Icon } from 'flavours/glitch/components/icon'; + import classNames from 'classnames'; + +import { connect } from 'react-redux'; + import { openModal, closeModal } from 'flavours/glitch/actions/modal'; +import { Icon } from 'flavours/glitch/components/icon'; +import { registrationsOpen } from 'flavours/glitch/initial_state'; const mapStateToProps = (state, { accountId }) => ({ displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']), + signupUrl: state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up', }); const mapDispatchToProps = (dispatch) => ({ onSignupClick() { - dispatch(closeModal()); - dispatch(openModal('CLOSED_REGISTRATIONS')); + dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })); + dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })); }, }); -class Copypaste extends React.PureComponent { +class Copypaste extends PureComponent { static propTypes = { value: PropTypes.string, @@ -74,13 +82,14 @@ class Copypaste extends React.PureComponent { } -class InteractionModal extends React.PureComponent { +class InteractionModal extends PureComponent { static propTypes = { displayNameHtml: PropTypes.string, url: PropTypes.string, type: PropTypes.oneOf(['reply', 'reblog', 'favourite', 'follow']), onSignupClick: PropTypes.func.isRequired, + signupUrl: PropTypes.string.isRequired, }; handleSignupClick = () => { @@ -88,7 +97,7 @@ class InteractionModal extends React.PureComponent { }; render () { - const { url, type, displayNameHtml } = this.props; + const { url, type, displayNameHtml, signupUrl } = this.props; const name = ; @@ -121,7 +130,7 @@ class InteractionModal extends React.PureComponent { if (registrationsOpen) { signupButton = ( - + ); @@ -143,7 +152,7 @@ class InteractionModal extends React.PureComponent {

    - + {signupButton}
    diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx index 7160e7efbf..14d483212e 100644 --- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx +++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx @@ -1,12 +1,16 @@ -import React from 'react'; -import Column from 'flavours/glitch/components/column'; -import { connect } from 'react-redux'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ColumnHeader from 'flavours/glitch/components/column_header'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import { Helmet } from 'react-helmet'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import Column from 'flavours/glitch/components/column'; +import ColumnHeader from 'flavours/glitch/components/column_header'; + + const messages = defineMessages({ heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' }, }); 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 64522a3a86..4dc9c1f893 100644 --- a/app/javascript/flavours/glitch/features/list_adder/components/account.jsx +++ b/app/javascript/flavours/glitch/features/list_adder/components/account.jsx @@ -1,12 +1,14 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { makeGetAccount } from '../../../selectors'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { Avatar } from '../../../components/avatar'; -import DisplayName from '../../../components/display_name'; import { injectIntl } from 'react-intl'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { Avatar } from '../../../components/avatar'; +import { DisplayName } from '../../../components/display_name'; +import { makeGetAccount } from '../../../selectors'; + + const makeMapStateToProps = () => { const getAccount = makeGetAccount(); diff --git a/app/javascript/flavours/glitch/features/list_adder/components/list.jsx b/app/javascript/flavours/glitch/features/list_adder/components/list.jsx index 587a953b01..83a5ecb91f 100644 --- a/app/javascript/flavours/glitch/features/list_adder/components/list.jsx +++ b/app/javascript/flavours/glitch/features/list_adder/components/list.jsx @@ -1,13 +1,16 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { IconButton } from '../../../components/icon_button'; + import { defineMessages, injectIntl } from 'react-intl'; -import { removeFromListAdder, addToListAdder } from '../../../actions/lists'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + import { Icon } from 'flavours/glitch/components/icon'; +import { removeFromListAdder, addToListAdder } from '../../../actions/lists'; +import { IconButton } from '../../../components/icon_button'; + const messages = defineMessages({ remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, add: { id: 'lists.account.add', defaultMessage: 'Add to list' }, diff --git a/app/javascript/flavours/glitch/features/list_adder/index.jsx b/app/javascript/flavours/glitch/features/list_adder/index.jsx index 45d5589f96..1ba9972e00 100644 --- a/app/javascript/flavours/glitch/features/list_adder/index.jsx +++ b/app/javascript/flavours/glitch/features/list_adder/index.jsx @@ -1,14 +1,17 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { injectIntl } from 'react-intl'; -import { setupListAdder, resetListAdder } from '../../actions/lists'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import List from './components/list'; -import Account from './components/account'; + +import { setupListAdder, resetListAdder } from '../../actions/lists'; import NewListForm from '../lists/components/new_list_form'; + +import Account from './components/account'; +import List from './components/list'; // hack const getOrderedLists = createSelector([state => state.get('lists')], lists => { 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 69fbaf5042..01c0444153 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/account.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/components/account.jsx @@ -1,12 +1,15 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; -import { IconButton } from 'flavours/glitch/components/icon_button'; + import { defineMessages } from 'react-intl'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Avatar } from 'flavours/glitch/components/avatar'; +import { DisplayName } from 'flavours/glitch/components/display_name'; +import { IconButton } from 'flavours/glitch/components/icon_button'; + + const messages = defineMessages({ remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, add: { id: 'lists.account.add', defaultMessage: 'Add to list' }, diff --git a/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.jsx b/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.jsx index 3f4e9ded60..5c4fff27b5 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/components/edit_list_form.jsx @@ -1,9 +1,12 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import { connect } from 'react-redux'; + import { changeListEditorTitle, submitListEditor } from 'flavours/glitch/actions/lists'; import { IconButton } from 'flavours/glitch/components/icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ title: { id: 'lists.edit.submit', defaultMessage: 'Change title' }, @@ -19,7 +22,7 @@ const mapDispatchToProps = dispatch => ({ onSubmit: () => dispatch(submitListEditor(false)), }); -class ListForm extends React.PureComponent { +class ListForm extends PureComponent { static propTypes = { value: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/list_editor/components/search.jsx b/app/javascript/flavours/glitch/features/list_editor/components/search.jsx index 3a51a7b4e5..a43b3b969e 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/search.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/components/search.jsx @@ -1,14 +1,17 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { defineMessages } from 'react-intl'; + import classNames from 'classnames'; + import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ search: { id: 'lists.search', defaultMessage: 'Search among people you follow' }, }); -export default class Search extends React.PureComponent { +export default class Search extends PureComponent { static propTypes = { intl: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js b/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js index 578bbc3e01..329a1dcb91 100644 --- a/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js +++ b/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js @@ -1,7 +1,10 @@ -import { connect } from 'react-redux'; -import { makeGetAccount } from 'flavours/glitch/selectors'; import { injectIntl } from 'react-intl'; + +import { connect } from 'react-redux'; + import { removeFromListEditor, addToListEditor } from 'flavours/glitch/actions/lists'; +import { makeGetAccount } from 'flavours/glitch/selectors'; + import Account from '../components/account'; const makeMapStateToProps = () => { diff --git a/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js b/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js index d11a6f82b3..a7f0009a84 100644 --- a/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js +++ b/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js @@ -1,5 +1,7 @@ -import { connect } from 'react-redux'; import { injectIntl } from 'react-intl'; + +import { connect } from 'react-redux'; + import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists'; import Search from '../components/search'; diff --git a/app/javascript/flavours/glitch/features/list_editor/index.jsx b/app/javascript/flavours/glitch/features/list_editor/index.jsx index 44951d1c64..134d5ff074 100644 --- a/app/javascript/flavours/glitch/features/list_editor/index.jsx +++ b/app/javascript/flavours/glitch/features/list_editor/index.jsx @@ -1,15 +1,21 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import spring from 'react-motion/lib/spring'; + import { setupListEditor, clearListSuggestions, resetListEditor } from 'flavours/glitch/actions/lists'; + +import Motion from '../ui/util/optional_motion'; + +import EditListForm from './components/edit_list_form'; import AccountContainer from './containers/account_container'; import SearchContainer from './containers/search_container'; -import EditListForm from './components/edit_list_form'; -import Motion from '../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; + const mapStateToProps = state => ({ accountIds: state.getIn(['listEditor', 'accounts', 'items']), diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.jsx b/app/javascript/flavours/glitch/features/list_timeline/index.jsx index f7abe248d4..d407e8a6ea 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/list_timeline/index.jsx @@ -1,9 +1,15 @@ import PropTypes from 'prop-types'; -import React from 'react'; -import { Helmet } from 'react-helmet'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { PureComponent } from 'react'; + import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; + +import Toggle from 'react-toggle'; + import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists'; import { openModal } from 'flavours/glitch/actions/modal'; @@ -12,10 +18,10 @@ import { expandListTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; import { Icon } from 'flavours/glitch/components/icon'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { RadioButton } from 'flavours/glitch/components/radio_button'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' }, @@ -30,7 +36,7 @@ const mapStateToProps = (state, props) => ({ hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0, }); -class ListTimeline extends React.PureComponent { +class ListTimeline extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -76,7 +82,7 @@ class ListTimeline extends React.PureComponent { this.disconnect = dispatch(connectListStream(id)); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { const { dispatch } = this.props; const { id } = nextProps.params; @@ -110,24 +116,30 @@ class ListTimeline extends React.PureComponent { }; handleEditClick = () => { - this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id })); + this.props.dispatch(openModal({ + modalType: 'LIST_EDITOR', + modalProps: { listId: this.props.params.id }, + })); }; handleDeleteClick = () => { const { dispatch, columnId, intl } = this.props; const { id } = this.props.params; - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => { - dispatch(deleteList(id)); + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.deleteMessage), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => { + dispatch(deleteList(id)); - if (columnId) { - dispatch(removeColumn(columnId)); - } else { - this.context.router.history.push('/lists'); - } + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + this.context.router.history.push('/lists'); + } + }, }, })); }; @@ -135,7 +147,13 @@ class ListTimeline extends React.PureComponent { handleRepliesPolicyChange = ({ target }) => { const { dispatch } = this.props; const { id } = this.props.params; - dispatch(updateList(id, undefined, false, target.value)); + dispatch(updateList(id, undefined, false, undefined, target.value)); + }; + + onExclusiveToggle = ({ target }) => { + const { dispatch } = this.props; + const { id } = this.props.params; + dispatch(updateList(id, undefined, false, target.checked, undefined)); }; render () { @@ -144,6 +162,7 @@ class ListTimeline extends React.PureComponent { const pinned = !!columnId; const title = list ? list.get('title') : id; const replies_policy = list ? list.get('replies_policy') : undefined; + const isExclusive = list ? list.get('exclusive') : undefined; if (typeof list === 'undefined') { return ( @@ -181,6 +200,13 @@ class ListTimeline extends React.PureComponent {
    +
    + + +
    + { replies_policy !== undefined && (
    diff --git a/app/javascript/flavours/glitch/features/lists/components/new_list_form.jsx b/app/javascript/flavours/glitch/features/lists/components/new_list_form.jsx index aa743dea4a..72602f1882 100644 --- a/app/javascript/flavours/glitch/features/lists/components/new_list_form.jsx +++ b/app/javascript/flavours/glitch/features/lists/components/new_list_form.jsx @@ -1,9 +1,12 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import { connect } from 'react-redux'; + import { changeListEditorTitle, submitListEditor } from 'flavours/glitch/actions/lists'; import { IconButton } from 'flavours/glitch/components/icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; const messages = defineMessages({ label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' }, @@ -20,7 +23,7 @@ const mapDispatchToProps = dispatch => ({ onSubmit: () => dispatch(submitListEditor(true)), }); -class NewListForm extends React.PureComponent { +class NewListForm extends PureComponent { static propTypes = { value: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/lists/index.jsx b/app/javascript/flavours/glitch/features/lists/index.jsx index dce0dcd8f8..e09d517d74 100644 --- a/app/javascript/flavours/glitch/features/lists/index.jsx +++ b/app/javascript/flavours/glitch/features/lists/index.jsx @@ -1,18 +1,22 @@ import PropTypes from 'prop-types'; -import React from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import { Helmet } from 'react-helmet'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; + import { fetchLists } from 'flavours/glitch/actions/lists'; import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import ScrollableList from 'flavours/glitch/components/scrollable_list'; import Column from 'flavours/glitch/features/ui/components/column'; import ColumnLink from 'flavours/glitch/features/ui/components/column_link'; import ColumnSubheading from 'flavours/glitch/features/ui/components/column_subheading'; + import NewListForm from './components/new_list_form'; const messages = defineMessages({ @@ -42,7 +46,7 @@ class Lists extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchLists()); } diff --git a/app/javascript/flavours/glitch/features/local_settings/index.jsx b/app/javascript/flavours/glitch/features/local_settings/index.jsx index 4e4605ea9d..d4f04c2108 100644 --- a/app/javascript/flavours/glitch/features/local_settings/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/index.jsx @@ -1,14 +1,16 @@ // Package imports. -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; // Our imports -import LocalSettingsPage from './page'; -import LocalSettingsNavigation from './navigation'; -import { closeModal } from 'flavours/glitch/actions/modal'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; +import { closeModal } from 'flavours/glitch/actions/modal'; + +import LocalSettingsNavigation from './navigation'; +import LocalSettingsPage from './page'; const mapStateToProps = state => ({ settings: state.get('local_settings'), @@ -19,11 +21,14 @@ const mapDispatchToProps = dispatch => ({ dispatch(changeLocalSetting(setting, value)); }, onClose () { - dispatch(closeModal()); + dispatch(closeModal({ + modalType: undefined, + ignoreFocus: false, + })); }, }); -class LocalSettings extends React.PureComponent { +class LocalSettings extends PureComponent { static propTypes = { onChange: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/index.jsx b/app/javascript/flavours/glitch/features/local_settings/navigation/index.jsx index fe08e5d7b0..022d817126 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.jsx @@ -1,12 +1,14 @@ // Package imports -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { injectIntl, defineMessages } from 'react-intl'; // Our imports -import LocalSettingsNavigationItem from './item'; import { preferencesLink } from 'flavours/glitch/utils/backend_links'; +import LocalSettingsNavigationItem from './item'; + // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * const messages = defineMessages({ @@ -19,7 +21,7 @@ const messages = defineMessages({ close: { id: 'settings.close', defaultMessage: 'Close' }, }); -class LocalSettingsNavigation extends React.PureComponent { +class LocalSettingsNavigation extends PureComponent { static propTypes = { index : PropTypes.number, diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.jsx b/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.jsx index 2de7c952c8..d1f3512ed7 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/item/index.jsx @@ -1,13 +1,14 @@ // Package imports -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import classNames from 'classnames'; import { Icon } from 'flavours/glitch/components/icon'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -export default class LocalSettingsPage extends React.PureComponent { +export default class LocalSettingsPage extends PureComponent { static propTypes = { active: PropTypes.bool, diff --git a/app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/index.jsx b/app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/index.jsx index 0f0dc3ce37..f266488ca4 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/index.jsx @@ -1,10 +1,10 @@ // Package imports -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -export default class LocalSettingsPageItem extends React.PureComponent { +export default class LocalSettingsPageItem extends PureComponent { static propTypes = { children: PropTypes.node.isRequired, diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx index 91582a3183..929391927f 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx @@ -1,14 +1,18 @@ // Package imports -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { PureComponent } from 'react'; + import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import ImmutablePropTypes from 'react-immutable-proptypes'; + + // Our imports import { expandSpoilers } from 'flavours/glitch/initial_state'; import { preferenceLink } from 'flavours/glitch/utils/backend_links'; -import LocalSettingsPageItem from './item'; + import DeprecatedLocalSettingsPageItem from './deprecated_item'; +import LocalSettingsPageItem from './item'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * @@ -31,11 +35,11 @@ const messages = defineMessages({ pop_in_right: { id: 'settings.pop_in_right', defaultMessage: 'Right' }, public: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); -class LocalSettingsPage extends React.PureComponent { +class LocalSettingsPage extends PureComponent { static propTypes = { index : PropTypes.number, @@ -271,7 +275,7 @@ class LocalSettingsPage extends React.PureComponent { ), ({ intl, onChange, settings }) => (
    -

    +

    + <> {account.get('display_name')} {account.get('username')} - + ); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/grant_permission_button.jsx b/app/javascript/flavours/glitch/features/notifications/components/grant_permission_button.jsx index 5b2db48fdb..cd46d878bb 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/grant_permission_button.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/grant_permission_button.jsx @@ -1,8 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; -export default class GrantPermissionButton extends React.PureComponent { +export default class GrantPermissionButton extends PureComponent { static propTypes = { onClick: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx index d1aea1b21d..1d476969cb 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx @@ -1,15 +1,17 @@ // Package imports. -import React from 'react'; import PropTypes from 'prop-types'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; // Our imports, import StatusContainer from 'flavours/glitch/containers/status_container'; -import NotificationFollow from './follow'; -import NotificationFollowRequestContainer from '../containers/follow_request_container'; -import NotificationAdminSignup from './admin_signup'; + import NotificationAdminReportContainer from '../containers/admin_report_container'; +import NotificationFollowRequestContainer from '../containers/follow_request_container'; + +import NotificationAdminSignup from './admin_signup'; +import NotificationFollow from './follow'; export default class Notification extends ImmutablePureComponent { diff --git a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx index 8868cc0cb4..b088935285 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.jsx @@ -1,18 +1,23 @@ -import React from 'react'; -import { Icon } from 'flavours/glitch/components/icon'; -import Button from 'flavours/glitch/components/button'; -import { IconButton } from 'flavours/glitch/components/icon_button'; +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { connect } from 'react-redux'; + import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'; import { changeSetting } from 'flavours/glitch/actions/settings'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; +import { Icon } from 'flavours/glitch/components/icon'; +import { IconButton } from 'flavours/glitch/components/icon_button'; + + const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, }); -class NotificationsPermissionBanner extends React.PureComponent { +class NotificationsPermissionBanner extends PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, diff --git a/app/javascript/flavours/glitch/features/notifications/components/overlay.jsx b/app/javascript/flavours/glitch/features/notifications/components/overlay.jsx index 00e6257380..73fb698643 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/overlay.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/overlay.jsx @@ -4,11 +4,13 @@ // Package imports. -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ diff --git a/app/javascript/flavours/glitch/features/notifications/components/pill_bar_button.jsx b/app/javascript/flavours/glitch/features/notifications/components/pill_bar_button.jsx index 2f0b48ef98..633401d6e3 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/pill_bar_button.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/pill_bar_button.jsx @@ -1,9 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { PureComponent } from 'react'; + import classNames from 'classnames'; -export default class PillBarButton extends React.PureComponent { +import ImmutablePropTypes from 'react-immutable-proptypes'; + +export default class PillBarButton extends PureComponent { static propTypes = { prefix: PropTypes.string, diff --git a/app/javascript/flavours/glitch/features/notifications/components/report.jsx b/app/javascript/flavours/glitch/features/notifications/components/report.jsx index b2e795b2d1..fee4f87fcf 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/report.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/report.jsx @@ -1,15 +1,19 @@ -import React, { Fragment } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; + import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; + import AvatarOverlay from 'flavours/glitch/components/avatar_overlay'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; +// This needs to be kept in sync with app/models/report.rb const messages = defineMessages({ openReport: { id: 'report_notification.open', defaultMessage: 'Open report' }, other: { id: 'report_notification.categories.other', defaultMessage: 'Other' }, spam: { id: 'report_notification.categories.spam', defaultMessage: 'Spam' }, + legal: { id: 'report_notification.categories.legal', defaultMessage: 'Legal' }, violation: { id: 'report_notification.categories.violation', defaultMessage: 'Rule violation' }, }); @@ -31,9 +35,9 @@ class Report extends ImmutablePureComponent { if (hidden) { return ( - + <> {report.get('id')} - + ); } diff --git a/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.jsx b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.jsx index dc7b89b7fd..2f849c5484 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.jsx @@ -1,9 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; + import Toggle from 'react-toggle'; -export default class SettingToggle extends React.PureComponent { +export default class SettingToggle extends PureComponent { static propTypes = { prefix: PropTypes.string, diff --git a/app/javascript/flavours/glitch/features/notifications/containers/admin_report_container.js b/app/javascript/flavours/glitch/features/notifications/containers/admin_report_container.js index 4198afce8b..62809b57b2 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/admin_report_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/admin_report_container.js @@ -1,5 +1,7 @@ import { connect } from 'react-redux'; + import { makeGetReport } from 'flavours/glitch/selectors'; + import AdminReport from '../components/admin_report'; const mapStateToProps = (state, { notification }) => { diff --git a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js index 27c2f96fe4..cc3793fccc 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js @@ -1,11 +1,14 @@ -import { connect } from 'react-redux'; import { defineMessages, injectIntl } from 'react-intl'; -import ColumnSettings from '../components/column_settings'; -import { changeSetting } from 'flavours/glitch/actions/settings'; + +import { connect } from 'react-redux'; + +import { showAlert } from 'flavours/glitch/actions/alerts'; +import { openModal } from 'flavours/glitch/actions/modal'; import { setFilter, clearNotifications, requestBrowserPermission } from 'flavours/glitch/actions/notifications'; import { changeAlerts as changePushNotifications } from 'flavours/glitch/actions/push_notifications'; -import { openModal } from 'flavours/glitch/actions/modal'; -import { showAlert } from 'flavours/glitch/actions/alerts'; +import { changeSetting } from 'flavours/glitch/actions/settings'; + +import ColumnSettings from '../components/column_settings'; const messages = defineMessages({ clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, @@ -57,10 +60,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onClear () { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(clearNotifications()), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.clearMessage), + confirm: intl.formatMessage(messages.clearConfirm), + onConfirm: () => dispatch(clearNotifications()), + }, })); }, diff --git a/app/javascript/flavours/glitch/features/notifications/containers/filter_bar_container.js b/app/javascript/flavours/glitch/features/notifications/containers/filter_bar_container.js index 4d495c2908..4e0184cef3 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/filter_bar_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/filter_bar_container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; -import FilterBar from '../components/filter_bar'; + import { setFilter } from '../../../actions/notifications'; +import FilterBar from '../components/filter_bar'; const makeMapStateToProps = state => ({ selectedFilter: state.getIn(['settings', 'notifications', 'quickFilter', 'active']), diff --git a/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js b/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js index 004d1e173b..d66b24991d 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/follow_request_container.js @@ -1,7 +1,9 @@ import { connect } from 'react-redux'; -import FollowRequest from '../components/follow_request'; + import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts'; +import FollowRequest from '../components/follow_request'; + const mapDispatchToProps = (dispatch, { account }) => ({ onAuthorize () { dispatch(authorizeFollowRequest(account.get('id'))); diff --git a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js index be007f30bb..b39d1169f2 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/notification_container.js @@ -2,9 +2,10 @@ import { connect } from 'react-redux'; // Our imports. -import { makeGetNotification } from 'flavours/glitch/selectors'; -import Notification from '../components/notification'; import { mentionCompose } from 'flavours/glitch/actions/compose'; +import { makeGetNotification } from 'flavours/glitch/selectors'; + +import Notification from '../components/notification'; const makeMapStateToProps = () => { const getNotification = makeGetNotification(); diff --git a/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js b/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js index ee2d19814a..e4b8902a81 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js @@ -2,9 +2,10 @@ import { connect } from 'react-redux'; // Our imports. -import NotificationOverlay from '../components/overlay'; import { markNotificationForDelete } from 'flavours/glitch/actions/notifications'; +import NotificationOverlay from '../components/overlay'; + const mapDispatchToProps = dispatch => ({ onMarkForDelete(id, yes) { dispatch(markNotificationForDelete(id, yes)); diff --git a/app/javascript/flavours/glitch/features/notifications/index.jsx b/app/javascript/flavours/glitch/features/notifications/index.jsx index c1042c1367..d84d29f112 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.jsx +++ b/app/javascript/flavours/glitch/features/notifications/index.jsx @@ -1,10 +1,20 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import classNames from 'classnames'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; +import { Helmet } from 'react-helmet'; + +import { List as ImmutableList } from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Column from 'flavours/glitch/components/column'; -import ColumnHeader from 'flavours/glitch/components/column_header'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { debounce } from 'lodash'; + +import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import { submitMarkers } from 'flavours/glitch/actions/markers'; import { enterNotificationClearingMode, expandNotifications, @@ -14,24 +24,24 @@ import { unmountNotifications, markNotificationsAsRead, } from 'flavours/glitch/actions/notifications'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; -import { submitMarkers } from 'flavours/glitch/actions/markers'; -import NotificationContainer from './containers/notification_container'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { compareId } from 'flavours/glitch/compare_id'; +import Column from 'flavours/glitch/components/column'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import { Icon } from 'flavours/glitch/components/icon'; +import { LoadGap } from 'flavours/glitch/components/load_gap'; +import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; +import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; + +import NotificationsPermissionBanner from './components/notifications_permission_banner'; import ColumnSettingsContainer from './containers/column_settings_container'; import FilterBarContainer from './containers/filter_bar_container'; -import { createSelector } from 'reselect'; -import { List as ImmutableList } from 'immutable'; -import { debounce } from 'lodash'; -import ScrollableList from 'flavours/glitch/components/scrollable_list'; -import LoadGap from 'flavours/glitch/components/load_gap'; -import { Icon } from 'flavours/glitch/components/icon'; -import { compareId } from 'flavours/glitch/compare_id'; -import NotificationsPermissionBanner from './components/notifications_permission_banner'; -import { NotSignedInIndicator } from 'flavours/glitch/components/not_signed_in_indicator'; -import { Helmet } from 'react-helmet'; +import NotificationContainer from './containers/notification_container'; + + + + -import NotificationPurgeButtonsContainer from 'flavours/glitch/containers/notification_purge_buttons_container'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, @@ -82,7 +92,7 @@ const mapDispatchToProps = dispatch => ({ dispatch, }); -class Notifications extends React.PureComponent { +class Notifications extends PureComponent { static contextTypes = { identity: PropTypes.object, @@ -323,12 +333,6 @@ class Notifications extends React.PureComponent {
    ); - const extraButton = ( - <> - {extraButtons} - - ); - return ( 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 01a823f173..7d5c4adb6f 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 @@ -1,17 +1,20 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { IconButton } from 'flavours/glitch/components/icon_button'; -import classNames from 'classnames'; -import { me, boostModal } from 'flavours/glitch/initial_state'; + import { defineMessages, injectIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { initBoostModal } from 'flavours/glitch/actions/boosts'; import { replyCompose } from 'flavours/glitch/actions/compose'; import { reblog, favourite, unreblog, unfavourite } from 'flavours/glitch/actions/interactions'; -import { makeGetStatus } from 'flavours/glitch/selectors'; import { openModal } from 'flavours/glitch/actions/modal'; -import { initBoostModal } from 'flavours/glitch/actions/boosts'; +import { IconButton } from 'flavours/glitch/components/icon_button'; +import { me, boostModal } from 'flavours/glitch/initial_state'; +import { makeGetStatus } from 'flavours/glitch/selectors'; const messages = defineMessages({ reply: { id: 'status.reply', defaultMessage: 'Reply' }, @@ -73,19 +76,25 @@ class Footer extends ImmutablePureComponent { if (signedIn) { if (askReplyConfirmation) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onConfirm: this._performReply, + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onConfirm: this._performReply, + }, })); } else { this._performReply(); } } else { - dispatch(openModal('INTERACTION', { - type: 'reply', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'reply', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; @@ -101,10 +110,13 @@ class Footer extends ImmutablePureComponent { dispatch(favourite(status)); } } else { - dispatch(openModal('INTERACTION', { - type: 'favourite', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'favourite', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; @@ -127,10 +139,13 @@ class Footer extends ImmutablePureComponent { dispatch(initBoostModal({ status, onReblog: this._performReblog })); } } else { - dispatch(openModal('INTERACTION', { - type: 'reblog', - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'reblog', + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); } }; 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 b9c72c276c..d58d77d7ab 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 @@ -1,14 +1,19 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { IconButton } from 'flavours/glitch/components/icon_button'; -import { Link } from 'react-router-dom'; -import { Avatar } from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; + import { defineMessages, injectIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { Avatar } from 'flavours/glitch/components/avatar'; +import { DisplayName } from 'flavours/glitch/components/display_name'; +import { IconButton } from 'flavours/glitch/components/icon_button'; + + + const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, }); diff --git a/app/javascript/flavours/glitch/features/picture_in_picture/index.jsx b/app/javascript/flavours/glitch/features/picture_in_picture/index.jsx index e6fb64ff99..ed229384c4 100644 --- a/app/javascript/flavours/glitch/features/picture_in_picture/index.jsx +++ b/app/javascript/flavours/glitch/features/picture_in_picture/index.jsx @@ -1,19 +1,23 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import Video from 'flavours/glitch/features/video'; -import Audio from 'flavours/glitch/features/audio'; -import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; -import Header from './components/header'; -import Footer from './components/footer'; +import { Component } from 'react'; + import classNames from 'classnames'; +import { connect } from 'react-redux'; + +import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; +import Audio from 'flavours/glitch/features/audio'; +import Video from 'flavours/glitch/features/video'; + +import Footer from './components/footer'; +import Header from './components/header'; + const mapStateToProps = state => ({ ...state.get('picture_in_picture'), left: state.getIn(['local_settings', 'media', 'pop_in_position']) === 'left', }); -class PictureInPicture extends React.Component { +class PictureInPicture extends Component { static propTypes = { statusId: PropTypes.string, diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js index 155afcca18..29f89b95a2 100644 --- a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js +++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js @@ -1,8 +1,10 @@ -import { connect } from 'react-redux'; -import { makeGetAccount } from 'flavours/glitch/selectors'; import { injectIntl } from 'react-intl'; + +import { connect } from 'react-redux'; + import { pinAccount, unpinAccount } from 'flavours/glitch/actions/accounts'; import Account from 'flavours/glitch/features/list_editor/components/account'; +import { makeGetAccount } from 'flavours/glitch/selectors'; const makeMapStateToProps = () => { const getAccount = makeGetAccount(); diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js index 0ada7b0401..010a7527de 100644 --- a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js +++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js @@ -1,11 +1,15 @@ -import { connect } from 'react-redux'; import { injectIntl } from 'react-intl'; + +import { connect } from 'react-redux'; + +import Search from 'flavours/glitch/features/list_editor/components/search'; + import { fetchPinnedAccountsSuggestions, clearPinnedAccountsSuggestions, changePinnedAccountsSuggestions, } from '../../../actions/accounts'; -import Search from 'flavours/glitch/features/list_editor/components/search'; + const mapStateToProps = state => ({ value: state.getIn(['pinnedAccountsEditor', 'suggestions', 'value']), diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.jsx b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.jsx index 834de652f5..603d7209a7 100644 --- a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.jsx +++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.jsx @@ -1,14 +1,18 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { injectIntl, FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import spring from 'react-motion/lib/spring'; + import { fetchPinnedAccounts, clearPinnedAccountsSuggestions, resetPinnedAccountsEditor } from 'flavours/glitch/actions/accounts'; +import Motion from 'flavours/glitch/features/ui/util/optional_motion'; + import AccountContainer from './containers/account_container'; import SearchContainer from './containers/search_container'; -import Motion from 'flavours/glitch/features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; const mapStateToProps = state => ({ accountIds: state.getIn(['pinnedAccountsEditor', 'accounts', 'items']), diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx index 41be2f7f33..bbb95e5522 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.jsx @@ -1,21 +1,26 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { fetchPinnedStatuses } from 'flavours/glitch/actions/pin_statuses'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; -import StatusList from 'flavours/glitch/components/status_list'; + import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { Helmet } from 'react-helmet'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { getStatusList } from 'flavours/glitch/selectors'; + +import { fetchPinnedStatuses } from '../../actions/pin_statuses'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import StatusList from '../../components/status_list'; +import Column from '../ui/components/column'; + const messages = defineMessages({ heading: { id: 'column.pins', defaultMessage: 'Pinned post' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'pins', 'items']), + statusIds: getStatusList(state, 'pins'), hasMore: !!state.getIn(['status_lists', 'pins', 'next']), }); @@ -29,7 +34,7 @@ class PinnedStatuses extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchPinnedStatuses()); } diff --git a/app/javascript/flavours/glitch/features/privacy_policy/index.jsx b/app/javascript/flavours/glitch/features/privacy_policy/index.jsx index a43befa73e..2b9fc013d8 100644 --- a/app/javascript/flavours/glitch/features/privacy_policy/index.jsx +++ b/app/javascript/flavours/glitch/features/privacy_policy/index.jsx @@ -1,16 +1,19 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { Helmet } from 'react-helmet'; +import { PureComponent } from 'react'; + import { FormattedMessage, FormattedDate, injectIntl, defineMessages } from 'react-intl'; -import Column from 'flavours/glitch/components/column'; + +import { Helmet } from 'react-helmet'; + import api from 'flavours/glitch/api'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import Column from 'flavours/glitch/components/column'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; const messages = defineMessages({ title: { id: 'privacy_policy.title', defaultMessage: 'Privacy Policy' }, }); -class PrivacyPolicy extends React.PureComponent { +class PrivacyPolicy extends PureComponent { static propTypes = { intl: PropTypes.object, diff --git a/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.jsx index a44d5c784d..2d083a90ec 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/public_timeline/components/column_settings.jsx @@ -1,7 +1,10 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + import SettingText from 'flavours/glitch/components/setting_text'; import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle'; @@ -9,7 +12,7 @@ const messages = defineMessages({ filter_regex: { id: 'home.column_settings.filter_regex', defaultMessage: 'Filter out by regular expressions' }, }); -class ColumnSettings extends React.PureComponent { +class ColumnSettings extends PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js index 97b7566585..b884f15f5f 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js @@ -1,7 +1,9 @@ import { connect } from 'react-redux'; -import ColumnSettings from '../components/column_settings'; -import { changeSetting } from 'flavours/glitch/actions/settings'; + import { changeColumnParams } from 'flavours/glitch/actions/columns'; +import { changeSetting } from 'flavours/glitch/actions/settings'; + +import ColumnSettings from '../components/column_settings'; const mapStateToProps = (state, { columnId }) => { const uuid = columnId; diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.jsx b/app/javascript/flavours/glitch/features/public_timeline/index.jsx index 737e5723f4..4e4b350f8b 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/public_timeline/index.jsx @@ -1,16 +1,21 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { connect } from 'react-redux'; + +import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import { connectPublicStream } from 'flavours/glitch/actions/streaming'; +import { expandPublicTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { expandPublicTimeline } from 'flavours/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectPublicStream } from 'flavours/glitch/actions/streaming'; -import { Helmet } from 'react-helmet'; import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; + +import ColumnSettingsContainer from './containers/column_settings_container'; const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Federated timeline' }, @@ -35,7 +40,7 @@ const mapStateToProps = (state, { columnId }) => { }; }; -class PublicTimeline extends React.PureComponent { +class PublicTimeline extends PureComponent { static defaultProps = { onlyMedia: false, @@ -141,11 +146,8 @@ class PublicTimeline extends React.PureComponent { - - - - } timelineId={`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`} onLoadMore={this.handleLoadMore} trackScroll={!pinned} diff --git a/app/javascript/flavours/glitch/features/reblogs/index.jsx b/app/javascript/flavours/glitch/features/reblogs/index.jsx index fe74f1fbcc..90d10db628 100644 --- a/app/javascript/flavours/glitch/features/reblogs/index.jsx +++ b/app/javascript/flavours/glitch/features/reblogs/index.jsx @@ -1,17 +1,24 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + import ImmutablePropTypes from 'react-immutable-proptypes'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + import { fetchReblogs } from 'flavours/glitch/actions/interactions'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import { Icon } from 'flavours/glitch/components/icon'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import ScrollableList from 'flavours/glitch/components/scrollable_list'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Column from 'flavours/glitch/features/ui/components/column'; -import { Icon } from 'flavours/glitch/components/icon'; -import ColumnHeader from 'flavours/glitch/components/column_header'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import ScrollableList from 'flavours/glitch/components/scrollable_list'; -import { Helmet } from 'react-helmet'; + + + + const messages = defineMessages({ heading: { id: 'column.reblogged_by', defaultMessage: 'Boosted by' }, @@ -32,13 +39,13 @@ class Reblogs extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - componentWillMount () { + UNSAFE_componentWillMount () { if (!this.props.accountIds) { this.props.dispatch(fetchReblogs(this.props.params.statusId)); } } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this.props.dispatch(fetchReblogs(nextProps.params.statusId)); } diff --git a/app/javascript/flavours/glitch/features/report/category.jsx b/app/javascript/flavours/glitch/features/report/category.jsx index 43e311f3d0..e734058fac 100644 --- a/app/javascript/flavours/glitch/features/report/category.jsx +++ b/app/javascript/flavours/glitch/features/report/category.jsx @@ -1,11 +1,15 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { List as ImmutableList } from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import Button from 'flavours/glitch/components/button'; + import Option from './components/option'; -import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' }, @@ -24,7 +28,7 @@ const mapStateToProps = state => ({ rules: state.getIn(['server', 'server', 'rules'], ImmutableList()), }); -class Category extends React.PureComponent { +class Category extends PureComponent { static propTypes = { onNextStep: PropTypes.func.isRequired, @@ -72,7 +76,7 @@ class Category extends React.PureComponent { ]; return ( - + <>

    @@ -95,7 +99,7 @@ class Category extends React.PureComponent {
    -
    + ); } diff --git a/app/javascript/flavours/glitch/features/report/comment.jsx b/app/javascript/flavours/glitch/features/report/comment.jsx index afcb7afa4f..db534a21fb 100644 --- a/app/javascript/flavours/glitch/features/report/comment.jsx +++ b/app/javascript/flavours/glitch/features/report/comment.jsx @@ -1,14 +1,17 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; -import Button from 'flavours/glitch/components/button'; + import Toggle from 'react-toggle'; +import Button from 'flavours/glitch/components/button'; + const messages = defineMessages({ placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' }, }); -class Comment extends React.PureComponent { +class Comment extends PureComponent { static propTypes = { onSubmit: PropTypes.func.isRequired, @@ -47,7 +50,7 @@ class Comment extends React.PureComponent { const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props; return ( - + <>