diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 04ac9560c..f991036ad 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 73fe22f3a..a2658ea8b 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -26,7 +26,6 @@ services: ports: - '127.0.0.1:3000:3000' - '127.0.0.1:4000:4000' - - '127.0.0.1:80:3000' networks: - external_network - internal_network @@ -70,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/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 7c3852e7e..a075cc7b3 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -3,14 +3,16 @@ set -e # Fail the whole script on first error # Fetch Ruby gem dependencies -bundle install --path vendor/bundle --with='development test' - -# Fetch Javascript dependencies -yarn install +bundle config path 'vendor/bundle' +bundle config with 'development test' +bundle install # Make Gemfile.lock pristine again git checkout -- Gemfile.lock +# Fetch Javascript dependencies +yarn --frozen-lockfile + # [re]create, migrate, and seed the test database RAILS_ENV=test ./bin/rails db:setup diff --git a/.eslintrc.js b/.eslintrc.js index bbdfa7de2..206faa1c7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,9 +4,12 @@ module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', + 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended', 'plugin:import/recommended', 'plugin:promise/recommended', + 'plugin:jsdoc/recommended', + 'plugin:prettier/recommended', ], env: { @@ -27,6 +30,7 @@ module.exports = { 'import', 'promise', '@typescript-eslint', + 'formatjs', ], parserOptions: { @@ -51,28 +55,14 @@ module.exports = { '\\.(css|scss|json)$', ], 'import/resolver': { - node: { - paths: ['app/javascript'], - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, + typescript: {}, }, }, rules: { - 'brace-style': 'warn', - 'comma-dangle': ['error', 'always-multiline'], - 'comma-spacing': [ - 'warn', - { - before: false, - after: true, - }, - ], - 'comma-style': ['warn', 'last'], 'consistent-return': 'error', 'dot-notation': 'error', - eqeqeq: 'error', - indent: ['warn', 2], + eqeqeq: ['error', 'always', { 'null': 'ignore' }], 'jsx-quotes': ['error', 'prefer-single'], 'no-case-declarations': 'off', 'no-catch-shadow': 'error', @@ -91,8 +81,16 @@ 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-trailing-spaces': 'warn', 'no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ @@ -100,34 +98,26 @@ module.exports = { { vars: 'all', args: 'after-used', + destructuredArrayIgnorePattern: '^_', ignoreRestSiblings: true, }, ], - 'object-curly-spacing': ['error', 'always'], - 'padded-blocks': [ - 'error', - { - classes: 'always', - }, - ], - quotes: ['error', 'single'], - semi: 'error', 'valid-typeof': 'error', 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }], 'react/jsx-boolean-value': 'error', - 'react/jsx-closing-bracket-location': ['error', 'line-aligned'], - 'react/jsx-curly-spacing': 'error', 'react/display-name': 'off', + 'react/jsx-fragments': ['error', 'syntax'], 'react/jsx-equals-spacing': 'error', - 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], - 'react/jsx-indent': ['error', 2], '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 @@ -190,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', { @@ -206,8 +199,63 @@ 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', @@ -218,6 +266,33 @@ module.exports = { 'promise/no-callback-in-promise': 'off', 'promise/no-nesting': 'off', 'promise/no-promise-in-callback': 'off', + + 'formatjs/blocklist-elements': 'error', + 'formatjs/enforce-default-message': ['error', 'literal'], + 'formatjs/enforce-description': 'off', // description values not currently used + 'formatjs/enforce-id': 'off', // Explicit IDs are used in the project + 'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx + 'formatjs/enforce-plural-rules': 'error', + 'formatjs/no-camel-case': 'off', // disabledAccount is only non-conforming + 'formatjs/no-complex-selectors': 'error', + 'formatjs/no-emoji': 'error', + 'formatjs/no-id': 'off', // IDs are used for translation keys + 'formatjs/no-invalid-icu': 'error', + 'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings + 'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx + 'formatjs/no-multiple-whitespaces': 'error', + 'formatjs/no-offset': 'error', + 'formatjs/no-useless-message': 'error', + 'formatjs/prefer-formatted-message': 'error', + 'formatjs/prefer-pound-in-plural': 'error', + + 'jsdoc/check-types': 'off', + 'jsdoc/no-undefined-types': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-returns': 'off', }, overrides: [ @@ -226,6 +301,8 @@ module.exports = { '*.config.js', '.*rc.js', 'ide-helper.js', + 'config/webpack/**/*', + 'config/formatjs-formatter.js', ], env: { @@ -235,6 +312,10 @@ module.exports = { parserOptions: { sourceType: 'script', }, + + rules: { + 'import/no-commonjs': 'off', + }, }, { files: [ @@ -245,15 +326,39 @@ 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-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', + + // Those rules set stricter rules for TS files + // to enforce better practices when converting from JS + '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', }, }, { @@ -266,5 +371,13 @@ module.exports = { jest: true, }, }, + { + files: [ + 'streaming/**/*', + ], + rules: { + 'import/no-commonjs': 'off', + }, + }, ], }; diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 000000000..1ae40d416 --- /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/build-image.yml b/.github/workflows/build-image.yml index 97a363d1e..da4203e35 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -43,9 +43,16 @@ jobs: type=edge,branch=main type=sha,prefix=,format=long + - name: Generate version suffix + id: version_vars + if: github.repository == 'mastodon/mastodon' && github.event_name == 'push' && github.ref_name == 'main' + run: | + echo mastodon_version_suffix=+edge-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT + - uses: docker/build-push-action@v4 with: context: . + build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} platforms: linux/amd64,linux/arm64 provenance: false builder: ${{ steps.buildx.outputs.name }} diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml new file mode 100644 index 000000000..f07f7447c --- /dev/null +++ b/.github/workflows/build-nightly.yml @@ -0,0 +1,60 @@ +name: Build nightly container image +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC +permissions: + contents: read + packages: write + +jobs: + build-nightly-image: + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v3 + - uses: hadolint/hadolint-action@v3.1.0 + - uses: docker/setup-qemu-action@v2 + - uses: docker/setup-buildx-action@v2 + + - name: Log in to the Github Container registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v4 + id: meta + with: + images: | + ghcr.io/mastodon/mastodon + flavor: | + latest=auto + tags: | + type=raw,value=nightly + type=schedule,pattern=nightly-{{date 'YYYY-MM-DD' tz='Etc/UTC'}} + labels: | + org.opencontainers.image.description=Nightly build image used for testing purposes + + - name: Generate version suffix + id: version_vars + run: | + echo mastodon_version_suffix=+nightly-$(date +'%Y%m%d') >> $GITHUB_OUTPUT + + - uses: docker/build-push-action@v4 + with: + context: . + build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} + platforms: linux/amd64,linux/arm64 + provenance: false + builder: ${{ steps.buildx.outputs.name }} + push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index aa8f1f584..b67c503e9 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -30,13 +30,27 @@ jobs: ruby-version: .ruby-version bundler-cache: true + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Check for missing strings in English JSON + run: | + yarn i18n:extract --throws + git diff --exit-code + - name: Check locale file normalization run: bundle exec i18n-tasks check-normalized - name: Check for unused strings run: bundle exec i18n-tasks unused - - name: Check for missing strings in English + - name: Check for missing strings in English YML run: | bundle exec i18n-tasks add-missing -l en git diff --exit-code diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index e13d227bd..4d3c2ce5a 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 2ddbca781..56d817123 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 44929f63d..1f0cfd1e7 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -3,25 +3,32 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' + - 'tsconfig.json' - '.nvmrc' - '.prettier*' - '.eslint*' - '**/*.js' - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' - '.github/workflows/lint-js.yml' pull_request: paths: - 'package.json' - 'yarn.lock' + - 'tsconfig.json' - '.nvmrc' - '.prettier*' - '.eslint*' - '**/*.js' - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' - '.github/workflows/lint-js.yml' jobs: @@ -42,4 +49,7 @@ jobs: run: yarn --frozen-lockfile - name: ESLint - run: yarn test:lint:js + run: yarn lint:js --max-warnings 0 + + - name: Typecheck + run: yarn typecheck diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml index 98f101ad9..8712d8bd8 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 6f76dd60c..d19a0470d 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 de54fe9ae..0395c8639 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 6f79babcf..295e9610b 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 6a8035210..131a62a57 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 6a1cacb3f..3306105f9 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -3,12 +3,15 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' - '.nvmrc' - '**/*.js' - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' - '**/*.snap' - '.github/workflows/test-js.yml' @@ -19,6 +22,8 @@ on: - '.nvmrc' - '**/*.js' - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' - '**/*.snap' - '.github/workflows/test-js.yml' @@ -40,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 d7e424a8c..a91fd819a 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 25bf5ba87..50266fb8a 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 d1aa8468a..07cb1d41f 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -4,12 +4,12 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' pull_request: env: BUNDLE_CLEAN: true BUNDLE_FROZEN: true - BUNDLE_WITHOUT: 'development production' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -19,8 +19,17 @@ jobs: build: runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + mode: + - production + - test env: - RAILS_ENV: test + RAILS_ENV: ${{ matrix.mode }} + BUNDLE_WITH: ${{ matrix.mode }} + OTP_SECRET: precompile_placeholder + SECRET_KEY_BASE: precompile_placeholder steps: - uses: actions/checkout@v3 @@ -50,6 +59,7 @@ jobs: ./bin/rails assets:precompile - uses: actions/upload-artifact@v3 + if: matrix.mode == 'test' with: path: |- ./public/assets @@ -97,14 +107,13 @@ jobs: PAM_ENABLED: true PAM_DEFAULT_SERVICE: pam_test PAM_CONTROLLED_SERVICE: pam_test_controlled - BUNDLE_WITH: 'pam_authentication' + BUNDLE_WITH: 'pam_authentication test' CI_JOBS: ${{ matrix.ci_job }}/4 strategy: fail-fast: false matrix: ruby-version: - - '2.7' - '3.0' - '3.1' - '.ruby-version' @@ -136,10 +145,6 @@ jobs: ruby-version: ${{ matrix.ruby-version}} bundler-cache: true - - name: Update system gems - if: matrix.ruby-version == '2.7' - run: gem update --system - - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' diff --git a/.haml-lint.yml b/.haml-lint.yml index 12ca46342..d1ed30b26 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/.nvmrc b/.nvmrc index 030fcd56b..59ea99ee6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.19 +16.20 diff --git a/.prettierignore b/.prettierignore index af0411e9c..27b6d5458 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/.prettierrc.js b/.prettierrc.js index 1d70813d5..af39b253f 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,3 +1,4 @@ module.exports = { - singleQuote: true + singleQuote: true, + jsxSingleQuote: true } diff --git a/.profile b/.profile index c6d57b609..f4826ea30 100644 --- a/.profile +++ b/.profile @@ -1 +1 @@ -LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread diff --git a/.rubocop.yml b/.rubocop.yml index e6a0c2d14..eff89bdae 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,9 +11,10 @@ require: - rubocop-rspec - rubocop-performance - rubocop-capybara + - ./lib/linter/rubocop_middle_dot AllCops: - TargetRubyVersion: 2.7 # Set to minimum supported version of CI + TargetRubyVersion: 3.0 # Set to minimum supported version of CI DisplayCopNames: true DisplayStyleGuide: true ExtraDetails: true @@ -43,7 +44,7 @@ Layout/LineLength: - !ruby/regexp / \# .*$/ - !ruby/regexp /^\# .*$/ Exclude: - - lib/**/*cli*.rb + - 'lib/mastodon/cli/*.rb' - db/*migrate/**/* - db/seeds/**/* @@ -53,123 +54,69 @@ 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: - - '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' - - '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/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_style.html#stylerescuestandarderror -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 +Rails/FilePath: + EnforcedStyle: arguments # Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus # https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus 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 @@ -259,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 2e4801a55..c1d580e51 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.48.1. +# 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,19 +21,6 @@ Layout/ArgumentAlignment: - 'config/initializers/cors.rb' - 'config/initializers/session_store.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, no_empty_lines -Layout/EmptyLinesAroundBlockBody: - Exclude: - - 'config/routes.rb' - -# This cop supports safe autocorrection (--autocorrect). -# 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 @@ -46,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: @@ -59,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 @@ -76,63 +49,18 @@ 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: AllowedMethods. -# AllowedMethods: enums -Lint/ConstantDefinitionInBlock: - Exclude: - - 'spec/controllers/api/base_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/concerns/accountable_concern_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/lib/activitypub/adapter_spec.rb' - - 'spec/lib/connection_pool/shared_connection_pool_spec.rb' - - 'spec/lib/connection_pool/shared_timed_stack_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - -# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches. -Lint/DuplicateBranch: - Exclude: - - 'app/lib/permalink_redirector.rb' - - 'app/models/account_statuses_filter.rb' - - 'app/validators/email_mx_validator.rb' - - 'app/validators/vote_validator.rb' - - 'lib/mastodon/maintenance_cli.rb' # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: - 'spec/controllers/api/v2/search_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - 'spec/fabricators/access_token_fabricator.rb' - 'spec/fabricators/conversation_fabricator.rb' - 'spec/fabricators/system_key_fabricator.rb' @@ -169,16 +97,6 @@ Lint/EmptyBlock: - 'spec/models/user_role_spec.rb' - 'spec/models/web/setting_spec.rb' -# Configuration parameters: AllowComments. -Lint/EmptyClass: - Exclude: - - 'spec/controllers/api/base_controller_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/NonDeterministicRequireOrder: - Exclude: - - 'spec/rails_helper.rb' - Lint/NonLocalExitFromIterator: Exclude: - 'app/helpers/jsonld_helper.rb' @@ -202,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' @@ -223,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: @@ -231,6 +151,8 @@ Lint/Void: # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 150 + Exclude: + - 'app/serializers/initial_state_serializer.rb' # Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: @@ -241,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/helpers/statuses_helper.rb' - - 'app/models/concerns/account_interactions.rb' - - 'app/models/concerns/has_user_settings.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: @@ -278,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: @@ -300,108 +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' - -# Configuration parameters: MinSize. -Performance/CollectionLiteralInLoop: - Exclude: - - 'app/models/admin/appeal_filter.rb' - - 'app/models/admin/status_filter.rb' - - 'app/models/relationship_filter.rb' - - 'app/models/trends/preview_card_filter.rb' - - 'app/models/trends/status_filter.rb' - - 'app/presenters/status_relationships_presenter.rb' - - 'app/services/fetch_resource_service.rb' - - 'app/services/suspend_account_service.rb' - - 'app/services/unsuspend_account_service.rb' - - 'config/deploy.rb' - - 'lib/mastodon/media_cli.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/Count: - Exclude: - - 'app/lib/importer/accounts_index_importer.rb' - - 'app/lib/importer/tags_index_importer.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/DeletePrefix: - Exclude: - - 'app/controllers/authorize_interactions_controller.rb' - - 'app/controllers/concerns/signature_verification.rb' - - 'app/controllers/intents_controller.rb' - - 'app/lib/activitypub/case_transform.rb' - - 'app/lib/permalink_redirector.rb' - - 'app/lib/webfinger_resource.rb' - - 'app/services/activitypub/fetch_remote_actor_service.rb' - - 'app/services/backup_service.rb' - - 'app/services/resolve_account_service.rb' - - 'app/services/tag_search_service.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -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' - -Performance/MethodObjectAsBlock: - Exclude: - - 'app/models/account_suggestions/source.rb' - - 'spec/models/export_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/RedundantEqualityComparisonBlock: - Exclude: - - 'spec/requests/link_headers_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: MaxKeyValuePairs. -Performance/RedundantMerge: - Exclude: - - 'config/initializers/paperclip.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/StartWith: - Exclude: - - 'app/lib/extractor.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: OnlySumOrWithInitialValue. -Performance/Sum: - Exclude: - - 'app/lib/activity_tracker.rb' - - 'app/models/trends/history.rb' - - 'lib/paperclip/color_extractor.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/TimesMap: - Exclude: - - 'spec/controllers/api/v1/blocks_controller_spec.rb' - - 'spec/controllers/api/v1/mutes_controller_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - - 'spec/lib/request_pool_spec.rb' - - 'spec/models/account_spec.rb' # This cop supports unsafe autocorrection (--autocorrect-all). Performance/UnfreezeString: @@ -431,192 +233,6 @@ RSpec/AnyInstance: - 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb' -RSpec/BeforeAfterAll: - Exclude: - - 'spec/requests/localization_spec.rb' - -# Configuration parameters: Prefixes, AllowedPatterns. -# Prefixes: when, with, without -RSpec/ContextWording: - Exclude: - - 'spec/config/initializers/rack_attack_spec.rb' - - 'spec/controllers/accounts_controller_spec.rb' - - 'spec/controllers/activitypub/collections_controller_spec.rb' - - 'spec/controllers/activitypub/inboxes_controller_spec.rb' - - 'spec/controllers/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/reports/actions_controller_spec.rb' - - 'spec/controllers/admin/statuses_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/relationships_controller_spec.rb' - - 'spec/controllers/api/v1/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/emails/confirmations_controller_spec.rb' - - 'spec/controllers/api/v1/instances/activity_controller_spec.rb' - - 'spec/controllers/api/v1/instances/peers_controller_spec.rb' - - 'spec/controllers/api/v1/media_controller_spec.rb' - - 'spec/controllers/api/v2/filters_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/controllers/auth/sessions_controller_spec.rb' - - 'spec/controllers/concerns/cache_concern_spec.rb' - - 'spec/controllers/concerns/challengable_concern_spec.rb' - - 'spec/controllers/concerns/localized_spec.rb' - - 'spec/controllers/concerns/rate_limit_headers_spec.rb' - - 'spec/controllers/instance_actors_controller_spec.rb' - - 'spec/controllers/settings/applications_controller_spec.rb' - - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' - - 'spec/controllers/statuses_controller_spec.rb' - - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb' - - 'spec/helpers/jsonld_helper_spec.rb' - - 'spec/helpers/routing_helper_spec.rb' - - 'spec/lib/activitypub/activity/accept_spec.rb' - - 'spec/lib/activitypub/activity/announce_spec.rb' - - 'spec/lib/activitypub/activity/create_spec.rb' - - 'spec/lib/activitypub/activity/follow_spec.rb' - - 'spec/lib/activitypub/activity/reject_spec.rb' - - 'spec/lib/emoji_formatter_spec.rb' - - 'spec/lib/entity_cache_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - - 'spec/lib/html_aware_formatter_spec.rb' - - 'spec/lib/link_details_extractor_spec.rb' - - 'spec/lib/ostatus/tag_manager_spec.rb' - - 'spec/lib/scope_transformer_spec.rb' - - 'spec/lib/status_cache_hydrator_spec.rb' - - 'spec/lib/status_reach_finder_spec.rb' - - 'spec/lib/text_formatter_spec.rb' - - 'spec/models/account/field_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/admin/account_action_spec.rb' - - 'spec/models/concerns/account_interactions_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - - 'spec/models/custom_emoji_filter_spec.rb' - - 'spec/models/custom_emoji_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/media_attachment_spec.rb' - - 'spec/models/notification_spec.rb' - - 'spec/models/remote_follow_spec.rb' - - 'spec/models/report_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/web/push_subscription_spec.rb' - - 'spec/policies/account_moderation_note_policy_spec.rb' - - 'spec/policies/account_policy_spec.rb' - - 'spec/policies/backup_policy_spec.rb' - - 'spec/policies/custom_emoji_policy_spec.rb' - - 'spec/policies/domain_block_policy_spec.rb' - - 'spec/policies/email_domain_block_policy_spec.rb' - - 'spec/policies/instance_policy_spec.rb' - - 'spec/policies/invite_policy_spec.rb' - - 'spec/policies/relay_policy_spec.rb' - - 'spec/policies/report_note_policy_spec.rb' - - 'spec/policies/report_policy_spec.rb' - - 'spec/policies/settings_policy_spec.rb' - - 'spec/policies/tag_policy_spec.rb' - - 'spec/policies/user_policy_spec.rb' - - 'spec/presenters/account_relationships_presenter_spec.rb' - - 'spec/presenters/status_relationships_presenter_spec.rb' - - 'spec/services/account_search_service_spec.rb' - - 'spec/services/account_statuses_cleanup_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_status_service_spec.rb' - - 'spec/services/activitypub/process_account_service_spec.rb' - - 'spec/services/activitypub/process_status_update_service_spec.rb' - - 'spec/services/fetch_link_card_service_spec.rb' - - 'spec/services/fetch_oembed_service_spec.rb' - - 'spec/services/fetch_remote_status_service_spec.rb' - - 'spec/services/follow_service_spec.rb' - - 'spec/services/import_service_spec.rb' - - 'spec/services/notify_service_spec.rb' - - 'spec/services/process_mentions_service_spec.rb' - - 'spec/services/reblog_service_spec.rb' - - 'spec/services/report_service_spec.rb' - - 'spec/services/resolve_account_service_spec.rb' - - 'spec/services/resolve_url_service_spec.rb' - - 'spec/services/search_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - - 'spec/services/verify_link_service_spec.rb' - - 'spec/validators/disallowed_hashtags_validator_spec.rb' - - 'spec/validators/email_mx_validator_spec.rb' - - 'spec/validators/follow_limit_validator_spec.rb' - - 'spec/validators/poll_validator_spec.rb' - - 'spec/validators/status_pin_validator_spec.rb' - - 'spec/validators/unreserved_username_validator_spec.rb' - - 'spec/validators/url_validator_spec.rb' - - 'spec/workers/move_worker_spec.rb' - - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# 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: @@ -656,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 @@ -698,7 +291,6 @@ RSpec/HookArgument: RSpec/InstanceVariable: Exclude: - 'spec/controllers/api/v1/streaming_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - 'spec/controllers/auth/confirmations_controller_spec.rb' - 'spec/controllers/auth/passwords_controller_spec.rb' - 'spec/controllers/auth/sessions_controller_spec.rb' @@ -708,7 +300,6 @@ RSpec/InstanceVariable: - 'spec/controllers/statuses_cleanup_controller_spec.rb' - 'spec/models/concerns/account_finder_concern_spec.rb' - 'spec/models/concerns/account_interactions_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - 'spec/models/public_feed_spec.rb' - 'spec/serializers/activitypub/note_serializer_spec.rb' - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' @@ -716,17 +307,6 @@ RSpec/InstanceVariable: - 'spec/services/search_service_spec.rb' - 'spec/services/unblock_domain_service_spec.rb' -RSpec/LeakyConstantDeclaration: - Exclude: - - 'spec/controllers/api/base_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/concerns/accountable_concern_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/lib/activitypub/adapter_spec.rb' - - 'spec/lib/connection_pool/shared_connection_pool_spec.rb' - - 'spec/lib/connection_pool/shared_timed_stack_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - RSpec/LetSetup: Exclude: - 'spec/controllers/admin/accounts_controller_spec.rb' @@ -736,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' @@ -752,7 +329,7 @@ RSpec/LetSetup: - 'spec/controllers/following_accounts_controller_spec.rb' - 'spec/controllers/oauth/authorized_applications_controller_spec.rb' - 'spec/controllers/oauth/tokens_controller_spec.rb' - - 'spec/controllers/tags_controller_spec.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' - 'spec/lib/activitypub/activity/delete_spec.rb' - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' - 'spec/models/account_spec.rb' @@ -767,6 +344,7 @@ RSpec/LetSetup: - 'spec/services/activitypub/process_collection_service_spec.rb' - 'spec/services/batched_remove_status_service_spec.rb' - 'spec/services/block_domain_service_spec.rb' + - 'spec/services/bulk_import_service_spec.rb' - 'spec/services/delete_account_service_spec.rb' - 'spec/services/import_service_spec.rb' - 'spec/services/notify_service_spec.rb' @@ -776,32 +354,8 @@ 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' -# This cop supports safe autocorrection (--autocorrect). -RSpec/MatchArray: - Exclude: - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/controllers/admin/export_domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/follower_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/following_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - - 'spec/controllers/api/v1/bookmarks_controller_spec.rb' - - 'spec/controllers/api/v1/favourites_controller_spec.rb' - - 'spec/controllers/api/v1/reports_controller_spec.rb' - - 'spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb' - - 'spec/models/account_filter_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/account_statuses_cleanup_policy_spec.rb' - - 'spec/models/custom_emoji_filter_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/presenters/familiar_followers_presenter_spec.rb' - - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' - - 'spec/services/update_status_service_spec.rb' - RSpec/MessageChain: Exclude: - 'spec/controllers/api/v1/media_controller_spec.rb' @@ -831,275 +385,20 @@ 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/application_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: Max: 21 -# This cop supports safe autocorrection (--autocorrect). -RSpec/MultipleSubjects: - Exclude: - - 'spec/controllers/activitypub/collections_controller_spec.rb' - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/controllers/activitypub/outboxes_controller_spec.rb' - - 'spec/controllers/api/web/embeds_controller_spec.rb' - - 'spec/controllers/emojis_controller_spec.rb' - - 'spec/controllers/follower_accounts_controller_spec.rb' - - 'spec/controllers/following_accounts_controller_spec.rb' - # Configuration parameters: AllowedGroups. 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' - - 'spec/models/user_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' - -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Inferences. -RSpec/Rails/InferredSpecType: - Exclude: - - 'spec/controllers/about_controller_spec.rb' - - 'spec/controllers/accounts_controller_spec.rb' - - 'spec/controllers/activitypub/collections_controller_spec.rb' - - 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb' - - 'spec/controllers/activitypub/inboxes_controller_spec.rb' - - 'spec/controllers/activitypub/outboxes_controller_spec.rb' - - 'spec/controllers/activitypub/replies_controller_spec.rb' - - 'spec/controllers/admin/account_moderation_notes_controller_spec.rb' - - 'spec/controllers/admin/accounts_controller_spec.rb' - - 'spec/controllers/admin/action_logs_controller_spec.rb' - - 'spec/controllers/admin/base_controller_spec.rb' - - 'spec/controllers/admin/change_emails_controller_spec.rb' - - 'spec/controllers/admin/confirmations_controller_spec.rb' - - 'spec/controllers/admin/dashboard_controller_spec.rb' - - 'spec/controllers/admin/disputes/appeals_controller_spec.rb' - - 'spec/controllers/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/email_domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/export_domain_allows_controller_spec.rb' - - 'spec/controllers/admin/export_domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/instances_controller_spec.rb' - - 'spec/controllers/admin/settings/branding_controller_spec.rb' - - 'spec/controllers/admin/tags_controller_spec.rb' - - 'spec/controllers/api/oembed_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/pins_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/search_controller_spec.rb' - - 'spec/controllers/api/v1/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb' - - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/admin/reports_controller_spec.rb' - - 'spec/controllers/api/v1/announcements/reactions_controller_spec.rb' - - 'spec/controllers/api/v1/announcements_controller_spec.rb' - - 'spec/controllers/api/v1/apps_controller_spec.rb' - - 'spec/controllers/api/v1/blocks_controller_spec.rb' - - 'spec/controllers/api/v1/bookmarks_controller_spec.rb' - - 'spec/controllers/api/v1/conversations_controller_spec.rb' - - 'spec/controllers/api/v1/custom_emojis_controller_spec.rb' - - 'spec/controllers/api/v1/domain_blocks_controller_spec.rb' - - 'spec/controllers/api/v1/emails/confirmations_controller_spec.rb' - - 'spec/controllers/api/v1/endorsements_controller_spec.rb' - - 'spec/controllers/api/v1/favourites_controller_spec.rb' - - 'spec/controllers/api/v1/filters_controller_spec.rb' - - 'spec/controllers/api/v1/follow_requests_controller_spec.rb' - - 'spec/controllers/api/v1/followed_tags_controller_spec.rb' - - 'spec/controllers/api/v1/instances/activity_controller_spec.rb' - - 'spec/controllers/api/v1/instances/peers_controller_spec.rb' - - 'spec/controllers/api/v1/instances_controller_spec.rb' - - 'spec/controllers/api/v1/lists_controller_spec.rb' - - 'spec/controllers/api/v1/markers_controller_spec.rb' - - 'spec/controllers/api/v1/media_controller_spec.rb' - - 'spec/controllers/api/v1/mutes_controller_spec.rb' - - 'spec/controllers/api/v1/notifications_controller_spec.rb' - - 'spec/controllers/api/v1/polls/votes_controller_spec.rb' - - 'spec/controllers/api/v1/polls_controller_spec.rb' - - 'spec/controllers/api/v1/reports_controller_spec.rb' - - 'spec/controllers/api/v1/statuses/favourited_by_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/statuses/reblogged_by_accounts_controller_spec.rb' - - 'spec/controllers/api/v1/statuses_controller_spec.rb' - - 'spec/controllers/api/v1/suggestions_controller_spec.rb' - - 'spec/controllers/api/v1/tags_controller_spec.rb' - - 'spec/controllers/api/v1/trends/tags_controller_spec.rb' - - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' - - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' - - 'spec/controllers/api/v2/filters_controller_spec.rb' - - 'spec/controllers/api/v2/search_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - - 'spec/controllers/auth/challenges_controller_spec.rb' - - 'spec/controllers/auth/confirmations_controller_spec.rb' - - 'spec/controllers/auth/passwords_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/controllers/auth/sessions_controller_spec.rb' - - 'spec/controllers/concerns/account_controller_concern_spec.rb' - - 'spec/controllers/concerns/cache_concern_spec.rb' - - 'spec/controllers/concerns/challengable_concern_spec.rb' - - 'spec/controllers/concerns/export_controller_concern_spec.rb' - - 'spec/controllers/concerns/localized_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/controllers/concerns/user_tracking_concern_spec.rb' - - 'spec/controllers/disputes/appeals_controller_spec.rb' - - 'spec/controllers/disputes/strikes_controller_spec.rb' - - 'spec/controllers/home_controller_spec.rb' - - 'spec/controllers/instance_actors_controller_spec.rb' - - 'spec/controllers/intents_controller_spec.rb' - - 'spec/controllers/oauth/authorizations_controller_spec.rb' - - 'spec/controllers/oauth/tokens_controller_spec.rb' - - 'spec/controllers/settings/imports_controller_spec.rb' - - 'spec/controllers/settings/profiles_controller_spec.rb' - - 'spec/controllers/statuses_cleanup_controller_spec.rb' - - 'spec/controllers/tags_controller_spec.rb' - - 'spec/controllers/well_known/host_meta_controller_spec.rb' - - 'spec/controllers/well_known/nodeinfo_controller_spec.rb' - - 'spec/controllers/well_known/webfinger_controller_spec.rb' - - 'spec/helpers/accounts_helper_spec.rb' - - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb' - - 'spec/helpers/admin/action_logs_helper_spec.rb' - - 'spec/helpers/flashes_helper_spec.rb' - - 'spec/helpers/formatting_helper_spec.rb' - - 'spec/helpers/home_helper_spec.rb' - - 'spec/helpers/routing_helper_spec.rb' - - 'spec/mailers/admin_mailer_spec.rb' - - 'spec/mailers/notification_mailer_spec.rb' - - 'spec/mailers/user_mailer_spec.rb' - - 'spec/models/account/field_spec.rb' - - 'spec/models/account_alias_spec.rb' - - 'spec/models/account_conversation_spec.rb' - - 'spec/models/account_deletion_request_spec.rb' - - 'spec/models/account_domain_block_spec.rb' - - 'spec/models/account_migration_spec.rb' - - 'spec/models/account_moderation_note_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/account_statuses_cleanup_policy_spec.rb' - - 'spec/models/admin/account_action_spec.rb' - - 'spec/models/admin/action_log_spec.rb' - - 'spec/models/announcement_mute_spec.rb' - - 'spec/models/announcement_reaction_spec.rb' - - 'spec/models/announcement_spec.rb' - - 'spec/models/backup_spec.rb' - - 'spec/models/block_spec.rb' - - 'spec/models/canonical_email_block_spec.rb' - - 'spec/models/conversation_mute_spec.rb' - - 'spec/models/conversation_spec.rb' - - 'spec/models/custom_emoji_spec.rb' - - 'spec/models/custom_filter_keyword_spec.rb' - - 'spec/models/custom_filter_spec.rb' - - 'spec/models/device_spec.rb' - - 'spec/models/domain_block_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/encrypted_message_spec.rb' - - 'spec/models/favourite_spec.rb' - - 'spec/models/featured_tag_spec.rb' - - 'spec/models/follow_recommendation_suppression_spec.rb' - - 'spec/models/follow_request_spec.rb' - - 'spec/models/follow_spec.rb' - - 'spec/models/home_feed_spec.rb' - - 'spec/models/identity_spec.rb' - - 'spec/models/import_spec.rb' - - 'spec/models/invite_spec.rb' - - 'spec/models/list_account_spec.rb' - - 'spec/models/list_spec.rb' - - 'spec/models/login_activity_spec.rb' - - 'spec/models/media_attachment_spec.rb' - - 'spec/models/mention_spec.rb' - - 'spec/models/mute_spec.rb' - - 'spec/models/notification_spec.rb' - - 'spec/models/poll_vote_spec.rb' - - 'spec/models/preview_card_spec.rb' - - 'spec/models/preview_card_trend_spec.rb' - - 'spec/models/public_feed_spec.rb' - - 'spec/models/relay_spec.rb' - - 'spec/models/scheduled_status_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/models/site_upload_spec.rb' - - 'spec/models/status_pin_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/status_stat_spec.rb' - - 'spec/models/status_trend_spec.rb' - - 'spec/models/system_key_spec.rb' - - 'spec/models/tag_follow_spec.rb' - - 'spec/models/unavailable_domain_spec.rb' - - 'spec/models/user_invite_request_spec.rb' - - 'spec/models/user_role_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/models/web/push_subscription_spec.rb' - - 'spec/models/web/setting_spec.rb' - - 'spec/models/webauthn_credentials_spec.rb' - - 'spec/models/webhook_spec.rb' - -RSpec/RepeatedExample: - 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: @@ -1115,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' @@ -1141,92 +439,10 @@ 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/controllers/settings/imports_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 safe autocorrection (--autocorrect). -# Configuration parameters: ExpectedOrder, Include. -# ExpectedOrder: index, show, new, edit, create, update, destroy -# Include: app/controllers/**/*.rb -Rails/ActionOrder: - Exclude: - - 'app/controllers/admin/announcements_controller.rb' - - 'app/controllers/admin/roles_controller.rb' - - 'app/controllers/admin/rules_controller.rb' - - 'app/controllers/admin/warning_presets_controller.rb' - - 'app/controllers/admin/webhooks_controller.rb' - - 'app/controllers/api/v1/admin/domain_allows_controller.rb' - - 'app/controllers/api/v1/admin/domain_blocks_controller.rb' - - 'app/controllers/api/v1/admin/email_domain_blocks_controller.rb' - - 'app/controllers/api/v1/admin/ip_blocks_controller.rb' - - 'app/controllers/api/v1/filters_controller.rb' - - 'app/controllers/api/v1/media_controller.rb' - - 'app/controllers/api/v1/push/subscriptions_controller.rb' - - 'app/controllers/api/v2/filters/keywords_controller.rb' - - 'app/controllers/api/v2/filters/statuses_controller.rb' - - 'app/controllers/api/v2/filters_controller.rb' - - 'app/controllers/auth/registrations_controller.rb' - - 'app/controllers/filters_controller.rb' - - 'app/controllers/settings/applications_controller.rb' - - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: app/models/**/*.rb -Rails/ActiveRecordCallbacksOrder: - Exclude: - - 'app/models/account.rb' - - 'app/models/account_conversation.rb' - - 'app/models/announcement_reaction.rb' - - 'app/models/block.rb' - - 'app/models/media_attachment.rb' - - 'app/models/session_activation.rb' - - 'app/models/status.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Rails/ApplicationController: Exclude: - 'app/controllers/health_controller.rb' - - 'app/controllers/well_known/host_meta_controller.rb' - - 'app/controllers/well_known/nodeinfo_controller.rb' - - 'app/controllers/well_known/webfinger_controller.rb' # Configuration parameters: Database, Include. # SupportedDatabases: mysql, postgresql @@ -1265,22 +481,6 @@ Rails/BulkChangeTable: - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' - 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/CompactBlank: - Exclude: - - 'app/helpers/application_helper.rb' - - 'app/helpers/statuses_helper.rb' - - 'app/models/concerns/attachmentable.rb' - - 'app/models/poll.rb' - - 'app/services/import_service.rb' - - 'config/initializers/paperclip.rb' - -# This cop supports safe autocorrection (--autocorrect). -Rails/ContentTag: - Exclude: - - 'app/helpers/application_helper.rb' - - 'app/helpers/branding_helper.rb' - # Configuration parameters: Include. # Include: db/migrate/*.rb Rails/CreateTableWithTimestamps: @@ -1294,12 +494,6 @@ Rails/CreateTableWithTimestamps: - 'db/migrate/20220824233535_create_status_trends.rb' - 'db/migrate/20221006061337_create_preview_card_trends.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Severity. -Rails/DeprecatedActiveModelErrorsMethods: - Exclude: - - 'lib/mastodon/accounts_cli.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Severity. Rails/DuplicateAssociation: @@ -1307,79 +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: EnforcedStyle. -# SupportedStyles: slashes, arguments -Rails/FilePath: - Exclude: - - 'app/lib/themes.rb' - - 'app/models/setting.rb' - - 'app/validators/reaction_validator.rb' - - 'config/environments/test.rb' - - 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb' - - 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb' - - 'db/migrate/20171028221157_add_reblogs_to_follows.rb' - - 'db/migrate/20171107143332_add_memorial_to_accounts.rb' - - 'db/migrate/20171107143624_add_disabled_to_users.rb' - - 'db/migrate/20171109012327_add_moderator_to_accounts.rb' - - 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb' - - 'db/migrate/20180615122121_add_autofollow_to_invites.rb' - - 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb' - - 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb' - - 'db/migrate/20181010141500_add_silent_to_mentions.rb' - - 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb' - - 'db/migrate/20181018205649_add_unread_to_account_conversations.rb' - - 'db/migrate/20181127130500_identity_id_to_bigint.rb' - - 'db/migrate/20181127165847_add_show_replies_to_lists.rb' - - 'db/migrate/20190201012802_add_overwrite_to_imports.rb' - - 'db/migrate/20190306145741_add_lock_version_to_polls.rb' - - 'db/migrate/20190307234537_add_approved_to_users.rb' - - 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb' - - 'db/migrate/20191212003415_increase_backup_size.rb' - - 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb' - - 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb' - - 'db/migrate/20200917192924_add_notify_to_follows.rb' - - 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb' - - 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb' - - 'db/migrate/20211231080958_add_category_to_reports.rb' - - 'db/migrate/20220613110834_add_action_to_custom_filters.rb' - - 'db/post_migrate/20220307083603_optimize_null_index_conversations_uri.rb' - - 'db/post_migrate/20220310060545_optimize_null_index_statuses_in_reply_to_account_id.rb' - - 'db/post_migrate/20220310060556_optimize_null_index_statuses_in_reply_to_id.rb' - - 'db/post_migrate/20220310060614_optimize_null_index_media_attachments_scheduled_status_id.rb' - - 'db/post_migrate/20220310060626_optimize_null_index_media_attachments_shortcode.rb' - - 'db/post_migrate/20220310060641_optimize_null_index_users_reset_password_token.rb' - - 'db/post_migrate/20220310060653_optimize_null_index_users_created_by_application_id.rb' - - 'db/post_migrate/20220310060706_optimize_null_index_statuses_uri.rb' - - 'db/post_migrate/20220310060722_optimize_null_index_accounts_moved_to_account_id.rb' - - 'db/post_migrate/20220310060740_optimize_null_index_oauth_access_tokens_refresh_token.rb' - - 'db/post_migrate/20220310060750_optimize_null_index_accounts_url.rb' - - 'db/post_migrate/20220310060809_optimize_null_index_oauth_access_tokens_resource_owner_id.rb' - - 'db/post_migrate/20220310060833_optimize_null_index_announcement_reactions_custom_emoji_id.rb' - - 'db/post_migrate/20220310060854_optimize_null_index_appeals_approved_by_account_id.rb' - - 'db/post_migrate/20220310060913_optimize_null_index_account_migrations_target_account_id.rb' - - 'db/post_migrate/20220310060926_optimize_null_index_appeals_rejected_by_account_id.rb' - - 'db/post_migrate/20220310060939_optimize_null_index_list_accounts_follow_id.rb' - - 'db/post_migrate/20220310060959_optimize_null_index_web_push_subscriptions_access_token_id.rb' - - 'db/post_migrate/20220613110802_remove_whole_word_from_custom_filters.rb' - - 'db/post_migrate/20220613110903_remove_irreversible_from_custom_filters.rb' - - 'db/post_migrate/20220617202502_migrate_roles.rb' - - 'db/seeds.rb' - - 'db/seeds/03_roles.rb' - - 'lib/tasks/branding.rake' - - 'lib/tasks/emojis.rake' - - 'lib/tasks/repo.rake' - - 'spec/controllers/admin/custom_emojis_controller_spec.rb' - - 'spec/fabricators/custom_emoji_fabricator.rb' - - 'spec/fabricators/site_upload_fabricator.rb' - - 'spec/rails_helper.rb' - - 'spec/spec_helper.rb' - # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasAndBelongsToMany: @@ -1403,47 +524,11 @@ Rails/HasManyOrHasOneDependent: - 'app/models/user.rb' - 'app/models/web/push_subscription.rb' -# Configuration parameters: Include. -# Include: app/helpers/**/*.rb -Rails/HelperInstanceVariable: - Exclude: - - 'app/helpers/application_helper.rb' - - 'app/helpers/instance_helper.rb' - - 'app/helpers/jsonld_helper.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: spec/**/*, test/**/* -Rails/HttpPositionalArguments: - Exclude: - - 'spec/config/initializers/rack_attack_spec.rb' - -# Configuration parameters: Include. -# Include: spec/**/*.rb, test/**/*.rb -Rails/I18nLocaleAssignment: - Exclude: - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/helpers/application_helper_spec.rb' - - 'spec/requests/localization_spec.rb' - Rails/I18nLocaleTexts: Exclude: - 'lib/tasks/mastodon.rake' - 'spec/helpers/flashes_helper_spec.rb' -# Configuration parameters: IgnoreScopes, Include. -# Include: app/models/**/*.rb -Rails/InverseOf: - Exclude: - - 'app/models/appeal.rb' - - 'app/models/concerns/account_interactions.rb' - - 'app/models/custom_emoji.rb' - - 'app/models/domain_block.rb' - - 'app/models/follow_recommendation.rb' - - 'app/models/instance.rb' - - 'app/models/notification.rb' - - 'app/models/status.rb' - # Configuration parameters: Include. # Include: app/controllers/**/*.rb, app/mailers/**/*.rb Rails/LexicallyScopedActionFilter: @@ -1464,30 +549,16 @@ 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' - 'app/workers/web/push_notification_worker.rb' - 'lib/paperclip/color_extractor.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Include. -# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb -Rails/Output: - Exclude: - - 'lib/mastodon/ip_blocks_cli.rb' - Rails/OutputSafety: Exclude: - 'config/initializers/simple_form.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: NotNilAndNotEmpty, NotBlank, UnlessBlank. -Rails/Present: - Exclude: - - 'config/initializers/content_security_policy.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Include. # Include: **/Rakefile, **/*.rake @@ -1500,15 +571,6 @@ Rails/RakeEnvironment: - 'lib/tasks/repo.rake' - 'lib/tasks/statistics.rake' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Include. -# Include: spec/controllers/**/*.rb, spec/requests/**/*.rb, test/controllers/**/*.rb, test/integration/**/*.rb -Rails/ResponseParsedBody: - Exclude: - - 'spec/controllers/follower_accounts_controller_spec.rb' - - 'spec/controllers/following_accounts_controller_spec.rb' - - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' - # Configuration parameters: Include. # Include: db/**/*.rb Rails/ReversibleMigration: @@ -1572,29 +634,38 @@ 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' - 'spec/services/update_account_service_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/SquishedSQLHeredocs: +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ThreeStateBooleanColumn: Exclude: - - 'db/migrate/20170920024819_status_ids_to_timestamp_ids.rb' - - 'db/migrate/20180608213548_reject_following_blocked_users.rb' - - 'db/post_migrate/20190519130537_remove_boosts_widening_audience.rb' - - 'lib/mastodon/snowflake.rb' - - 'lib/tasks/tests.rake' - -Rails/TransactionExitStatement: - Exclude: - - 'app/lib/activitypub/activity/announce.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/activitypub/activity/delete.rb' - - 'app/services/activitypub/process_account_service.rb' + - 'db/migrate/20160325130944_add_admin_to_users.rb' + - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb' + - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170330163835_create_imports.rb' + - 'db/migrate/20170905165803_add_local_to_statuses.rb' + - 'db/migrate/20171210213213_add_local_only_flag_to_statuses.rb' + - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb' + - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb' + - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' + - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb' + - 'db/migrate/20200309150742_add_forwarded_to_reports.rb' + - 'db/migrate/20210609202149_create_login_activities.rb' + - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb' + - 'db/migrate/20211031031021_create_preview_card_providers.rb' + - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' + - 'db/migrate/20220202200926_add_trendable_to_statuses.rb' + - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' # Configuration parameters: Include. # Include: app/models/**/*.rb @@ -1647,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' @@ -1663,12 +730,6 @@ Style/CaseEquality: Exclude: - 'config/initializers/trusted_proxies.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: MinBranchesCount. -Style/CaseLikeIf: - Exclude: - - 'app/controllers/concerns/signature_verification.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? @@ -1681,21 +742,16 @@ Style/ClassVars: Exclude: - 'config/initializers/devise.rb' +# This cop supports unsafe autocorrection (--autocorrect-all). Style/CombinableLoops: Exclude: - 'app/models/form/custom_emoji_batch.rb' - 'app/models/form/ip_block_batch.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ConcatArrayLiterals: - Exclude: - - 'app/lib/feed_manager.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. Style/FetchEnvVar: Exclude: - - 'app/helpers/application_helper.rb' - 'app/lib/redis_configuration.rb' - 'app/lib/translation_service.rb' - 'config/environments/development.rb' @@ -1721,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). @@ -2131,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: @@ -2145,7 +1195,6 @@ Style/GuardClause: - 'app/controllers/auth/passwords_controller.rb' - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - 'app/lib/activitypub/activity/block.rb' - - 'app/lib/connection_pool/shared_connection_pool.rb' - 'app/lib/request.rb' - 'app/lib/request_pool.rb' - 'app/lib/webfinger.rb' @@ -2167,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' @@ -2180,7 +1229,6 @@ Style/HashAsLastArrayItem: Exclude: - 'app/controllers/admin/statuses_controller.rb' - 'app/controllers/api/v1/statuses_controller.rb' - - 'app/models/account.rb' - 'app/models/concerns/account_counters.rb' - 'app/models/concerns/status_threading_concern.rb' - 'app/models/status.rb' @@ -2188,19 +1236,6 @@ Style/HashAsLastArrayItem: - 'app/services/notify_service.rb' - 'db/migrate/20181024224956_migrate_account_conversations.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, EnforcedShorthandSyntax, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. -# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys -# SupportedShorthandSyntax: always, never, either, consistent -Style/HashSyntax: - Exclude: - - 'app/helpers/application_helper.rb' - - 'app/models/media_attachment.rb' - - 'lib/terrapin/multi_pipe_extensions.rb' - - 'spec/controllers/admin/reports/actions_controller_spec.rb' - - 'spec/controllers/admin/statuses_controller_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). Style/HashTransformValues: Exclude: @@ -2218,22 +1253,8 @@ Style/IfUnlessModifier: # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: Exclude: - - 'app/controllers/concerns/signature_verification.rb' - - 'app/helpers/jsonld_helper.rb' - - 'app/lib/activitypub/activity/create.rb' - - 'app/lib/activitypub/activity/move.rb' - - 'app/lib/feed_manager.rb' - - 'app/lib/link_details_extractor.rb' - - 'app/models/concerns/attachmentable.rb' - - 'app/models/concerns/remotable.rb' - 'app/models/custom_filter.rb' - - 'app/models/webhook.rb' - - 'app/services/activitypub/process_status_update_service.rb' - - 'app/services/fetch_link_card_service.rb' - - 'app/services/search_service.rb' - 'app/services/update_account_service.rb' - - 'app/workers/web/push_notification_worker.rb' - - 'lib/paperclip/color_extractor.rb' - 'spec/controllers/activitypub/replies_controller_spec.rb' # This cop supports safe autocorrection (--autocorrect). @@ -2254,29 +1275,16 @@ Style/MapToHash: # SupportedStyles: literals, strict Style/MutableConstant: Exclude: - - 'app/lib/link_details_extractor.rb' - - 'app/models/account.rb' - - 'app/models/custom_emoji.rb' - 'app/models/tag.rb' - - 'app/services/account_search_service.rb' - 'app/services/delete_account_service.rb' - - 'app/services/fetch_link_card_service.rb' - - 'app/services/resolve_url_service.rb' - 'config/initializers/twitter_regex.rb' - - 'lib/mastodon/snowflake.rb' - - 'spec/controllers/api/base_controller_spec.rb' + - 'lib/mastodon/migration_warning.rb' # This cop supports safe autocorrection (--autocorrect). Style/NilLambda: Exclude: - 'config/initializers/paperclip.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns. -Style/NumericLiterals: - Exclude: - - 'config/initializers/strong_migrations.rb' - # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: @@ -2311,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' @@ -2325,53 +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/permalink_redirector.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! @@ -2386,7 +1346,6 @@ Style/Semicolon: Exclude: - 'spec/services/activitypub/process_status_update_service_spec.rb' - 'spec/validators/blacklisted_email_validator_spec.rb' - - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -2477,11 +1436,14 @@ Style/TrailingCommaInHashLiteral: - 'config/environments/test.rb' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: WordRegex. +# Configuration parameters: EnforcedStyle, MinSize, WordRegex. # SupportedStyles: percent, brackets Style/WordArray: - EnforcedStyle: percent - MinSize: 6 + Exclude: + - 'app/helpers/languages_helper.rb' + - 'config/initializers/cors.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' + - 'spec/models/form/import_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. diff --git a/Aptfile b/Aptfile index 8f5bb72a2..5e033f136 100644 --- a/Aptfile +++ b/Aptfile @@ -1,4 +1,5 @@ ffmpeg +libopenblas0-pthread libpq-dev libxdamage1 libxfixes3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4527c50d9..d6f1b7bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,76 @@ All notable changes to this project will be documented in this file. +## [4.1.4] - 2023-07-07 + +### Fixed + +- Fix branding:generate_app_icons failing because of disallowed ICO coder ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25794)) +- Fix crash in admin interface when viewing a remote user with verified links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25796)) +- Fix processing of media files with unusual names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25788)) + +## [4.1.3] - 2023-07-06 + +### Added + +- Add fallback redirection when getting a webfinger query `LOCAL_DOMAIN@LOCAL_DOMAIN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23600)) + +### Changed + +- Change OpenGraph-based embeds to allow fullscreen ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25058)) +- Change AccessTokensVacuum to also delete expired tokens ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868)) +- Change profile updates to be sent to recently-mentioned servers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24852)) +- Change automatic post deletion thresholds and load detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24614)) +- Change `/api/v1/statuses/:id/history` to always return at least one item ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25510)) +- Change auto-linking to allow carets in URL query params ([renchap](https://github.com/mastodon/mastodon/pull/25216)) + +### Removed + +- Remove invalid `X-Frame-Options: ALLOWALL` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25070)) + +### Fixed + +- Fix wrong view being displayed when a webhook fails validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25464)) +- Fix soft-deleted post cleanup scheduler overwhelming the streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25519)) +- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477)) +- Fix multiple inefficiencies in automatic post cleanup worker ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24785), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24840)) +- Fix performance of streaming by parsing message JSON once ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25278), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25361)) +- Fix CSP headers when `S3_ALIAS_HOST` includes a path component ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25273)) +- Fix `tootctl accounts approve --number N` not approving N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605)) +- Fix reports not being closed when performing batch suspensions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24988)) +- Fix being able to vote on your own polls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25015)) +- Fix race condition when reblogging a status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25016)) +- Fix “Authorized applications” inefficiently and incorrectly getting last use date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25060)) +- Fix “Authorized applications” crashing when listing apps with certain admin API scopes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25713)) +- Fix multiple N+1s in ConversationsController ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25134), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25399), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25499)) +- Fix user archive takeouts when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24431)) +- Fix searching for remote content by URL not working under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637)) +- Fix inefficiencies in indexing content for search ([VyrCossont](https://github.com/mastodon/mastodon/pull/24285), [VyrCossont](https://github.com/mastodon/mastodon/pull/24342)) + +### Security + +- Add finer permission requirements for managing webhooks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25463)) +- Update dependencies +- Add hardening headers for user-uploaded files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25756)) +- Fix verified links possibly hiding important parts of the URL (CVE-2023-36462) +- Fix timeout handling of outbound HTTP requests (CVE-2023-36461) +- Fix arbitrary file creation through media processing (CVE-2023-36460) +- Fix possible XSS in preview cards (CVE-2023-36459) + +## [4.1.2] - 2023-04-04 + +### Fixed + +- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377)) +- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302)) +- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200)) +- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337)) + +### Security + +- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24334)) +- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379)) + ## [4.1.1] - 2023-03-16 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7c0a39b8..a232915b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -# Contributing to Mastodon Glitch Edition # +# Contributing to Mastodon Glitch Edition Thank you for your interest in contributing to the `glitch-soc` project! Here are some guidelines, and ways you can help. -> (This document is a bit of a work-in-progress, so please bear with us. -> If you don't see what you're looking for here, please don't hesitate to reach out!) +> (This document is a bit of a work-in-progress, so please bear with us. +> If you don't see what you're looking for here, please don't hesitate to reach out!) ## Translations @@ -12,26 +12,26 @@ You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.co [![Crowdin](https://badges.crowdin.net/glitch-soc/localized.svg)](https://crowdin.com/project/glitch-soc) -## Planning ## +## Planning Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects. We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler. -## Documentation ## +## Documentation The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)). Right now, we've mostly focused on the features that make this fork different from upstream in some manner. Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code. -## Frontend Development ## +## Frontend Development Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information. -## Backend Development ## +## Backend Development See the guidelines below. - - - - +--- You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `mastodon/mastodon`, reproduced below. diff --git a/Dockerfile b/Dockerfile index dca547c6d..cb5096581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1.4 # This needs to be bullseye-slim because the Ruby image is built on bullseye-slim -ARG NODE_VERSION="16.19-bullseye-slim" +ARG NODE_VERSION="16.20-bullseye-slim" FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby FROM node:${NODE_VERSION} as build @@ -18,7 +18,6 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/ # hadolint ignore=DL3008 RUN apt-get update && \ apt-get install -y --no-install-recommends build-essential \ - ca-certificates \ git \ libicu-dev \ libidn11-dev \ @@ -42,6 +41,10 @@ RUN apt-get update && \ FROM node:${NODE_VERSION} +# Use those args to specify your own version flags & suffixes +ARG MASTODON_VERSION_FLAGS="" +ARG MASTODON_VERSION_SUFFIX="" + ARG UID="991" ARG GID="991" @@ -52,7 +55,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 && \ @@ -85,7 +88,9 @@ COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon ENV RAILS_ENV="production" \ NODE_ENV="production" \ RAILS_SERVE_STATIC_FILES="true" \ - BIND="0.0.0.0" + BIND="0.0.0.0" \ + MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \ + MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}" # Set the run user USER mastodon diff --git a/Gemfile b/Gemfile index 46f3e0106..7a0fbdc82 100644 --- a/Gemfile +++ b/Gemfile @@ -1,26 +1,24 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 2.7.0', '< 3.3.0' +ruby '>= 3.0.0' -gem 'pkg-config', '~> 1.5' - -gem 'puma', '~> 6.1' +gem 'puma', '~> 6.3' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' -gem 'rack', '~> 2.2.6' +gem 'rack', '~> 2.2.7' gem 'haml-rails', '~>2.0' -gem 'pg', '~> 1.4' +gem 'pg', '~> 1.5' gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.119', 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' @@ -28,15 +26,15 @@ gem 'addressable', '~> 2.8' gem 'bootsnap', '~> 1.16.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' -gem 'chewy', '~> 7.2' +gem 'chewy', '~> 7.3' gem 'devise', '~> 4.9' -gem 'devise-two-factor', '~> 4.0' +gem 'devise-two-factor', '~> 4.1' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' end -gem 'net-ldap', '~> 0.17' +gem 'net-ldap', '~> 0.18' gem 'omniauth-cas', '~> 2.0' gem 'omniauth-saml', '~> 1.10' gem 'omniauth_openid_connect', '~> 0.6.1' @@ -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,8 +72,8 @@ 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 'ruby-progressbar', '~> 1.11' +gem 'rqrcode', '~> 2.2' +gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' @@ -99,54 +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.38' - gem 'climate_control' - gem 'faker', '~> 3.1' - gem 'json-schema', '~> 3.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 + + # Profiling tools + gem 'memory_profiler', require: false + gem 'stackprof', require: false end group :production do @@ -157,7 +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 683539844..985e36c20 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,28 +61,28 @@ 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) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.1) + addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) airbrussh (1.4.1) @@ -104,31 +92,31 @@ GEM activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - attr_encrypted (3.1.0) + attr_encrypted (4.0.0) encryptor (~> 3.0.0) attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.735.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.119.2) - 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) @@ -142,7 +130,7 @@ GEM blurhash (0.1.7) bootsnap (1.16.0) msgpack (~> 1.2) - brakeman (5.4.0) + brakeman (5.4.1) browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -151,12 +139,12 @@ 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) sshkit (>= 1.9.0) - capistrano-bundler (2.0.1) + capistrano-bundler (2.1.0) capistrano (~> 3.1) capistrano-rails (1.6.2) capistrano (~> 3.1) @@ -166,7 +154,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.38.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) @@ -179,36 +167,35 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.2.7) + chewy (7.3.2) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl 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.3.0) + connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) crack (0.4.5) rexml crass (1.0.6) - css_parser (1.12.0) + css_parser (1.14.0) addressable date (3.3.3) - debug_inspector (1.0.0) + debug_inspector (1.1.0) devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (4.0.2) + devise-two-factor (4.1.0) activesupport (< 7.1) - attr_encrypted (>= 1.3, < 4, != 2) + attr_encrypted (>= 1.3, < 5, != 2) devise (~> 4.0) railties (< 7.1) rotp (~> 6.0) @@ -241,9 +228,9 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.95.0) + excon (0.100.0) fabrication (2.30.0) - faker (3.1.1) + faker (3.2.0) i18n (>= 1.8.11, < 2) faraday (1.10.3) faraday-em_http (~> 1.0) @@ -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) @@ -314,7 +301,7 @@ GEM hashie (5.0.0) hcaptcha (7.1.0) json - highline (2.0.3) + highline (2.1.0) hiredis (0.6.3) hkdf (0.3.0) htmlentities (4.3.4) @@ -331,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) @@ -348,26 +335,26 @@ GEM ipaddress (0.8.3) jmespath (1.6.2) json (2.6.3) - json-canonicalization (0.3.0) + json-canonicalization (0.3.2) json-jwt (1.15.3) activesupport (>= 4.2) aes_key_wrap bindata httpclient - json-ld (3.2.3) + json-ld (3.2.5) htmlentities (~> 4.3) - json-canonicalization (~> 0.3) + json-canonicalization (~> 0.3, >= 0.3.2) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) - rack (~> 2.2) - rdf (~> 3.2, >= 3.2.9) + rack (>= 2.2, < 4) + rdf (~> 3.2, >= 3.2.10) json-ld-preloaded (3.2.2) json-ld (~> 3.2) rdf (~> 3.2) - json-schema (3.0.0) + json-schema (4.0.0) addressable (>= 2.8) jsonapi-renderer (0.2.2) - jwt (2.7.0) + jwt (2.7.1) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -380,8 +367,14 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - launchy (2.5.0) - addressable (~> 2.7) + 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) launchy (>= 2.2, < 3) letter_opener_web (2.0.0) @@ -398,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 @@ -416,19 +409,19 @@ GEM method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.0218.1) mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.18.0) - msgpack (1.6.0) + 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.17.1) + net-ldap (0.18.0) net-pop (0.1.2) net-protocol net-protocol (0.2.1) @@ -437,17 +430,12 @@ GEM net-ssh (>= 2.6.5, < 8.0.0) net-smtp (0.3.3) net-protocol - net-ssh (7.0.1) - nio4r (2.5.8) - nokogiri (1.14.2) - mini_portile2 (~> 2.8.0) + net-ssh (7.1.0) + nio4r (2.5.9) + nokogiri (1.15.3) + 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.2) + oj (3.15.0) omniauth (1.9.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -479,19 +467,19 @@ GEM openssl-signature_algorithm (1.3.0) openssl (> 2.0) orm_adapter (0.5.0) - ox (2.14.14) - parallel (1.22.1) - parser (3.2.1.1) + ox (2.14.16) + parallel (1.23.0) + parser (3.2.2.3) ast (~> 2.4.1) + racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.4.6) - pghero (3.3.1) + pg (1.5.3) + pghero (3.3.3) activerecord (>= 6) - pkg-config (1.5.1) posix-spawn (0.3.15) - premailer (1.18.0) + premailer (1.21.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -501,13 +489,13 @@ GEM premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) public_suffix (5.0.1) - puma (6.1.1) + puma (6.3.0) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.2) - rack (2.2.6.4) + racc (1.7.1) + rack (2.2.7) rack-attack (6.6.1) rack (>= 1.0, < 3) rack-cors (2.0.1) @@ -522,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) @@ -544,112 +532,116 @@ 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.9) + 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.7.0) + regexp_parser (2.8.1) request_store (1.5.1) rack (>= 1.4) responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) rexml (3.2.5) - rotp (6.2.0) + 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) - rspec-core (3.12.1) + rspec-core (3.12.2) rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.3) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.1) + 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.48.1) + 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.26.0, < 2.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.27.0) + rubocop-ast (1.29.0) parser (>= 3.2.1.0) - rubocop-capybara (2.17.1) + rubocop-capybara (2.18.0) rubocop (~> 1.41) - rubocop-performance (1.16.0) + 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.18.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) - rufus-scheduler (3.8.2) + rubyzip (2.3.2) + rufus-scheduler (3.9.1) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) - sanitize (6.0.1) + sanitize (6.0.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) scenic (1.7.0) 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) @@ -681,8 +673,7 @@ GEM sshkit (1.21.4) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.24) - statsd-ruby (1.5.0) + stackprof (0.2.25) stoplight (3.0.1) redlock (~> 1.0) strong_migrations (0.8.0) @@ -692,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) @@ -725,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) @@ -762,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 @@ -771,7 +762,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.119) + aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) @@ -783,23 +774,23 @@ DEPENDENCIES capistrano-rails (~> 1.6) capistrano-rbenv (~> 2.2) capistrano-yarn (~> 2.0) - capybara (~> 3.38) + capybara (~> 3.39) charlock_holmes (~> 0.7.7) - chewy (~> 7.2) - climate_control + chewy (~> 7.3) + climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby connection_pool devise (~> 4.9) - devise-two-factor (~> 4.0) + devise-two-factor (~> 4.1) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) doorkeeper (~> 5.6) dotenv-rails (~> 2.8) ed25519 (~> 1.3) fabrication (~> 2.30) - faker (~> 3.1) + faker (~> 3.2) fast_blank (~> 1.0) fastimage fog-core (<= 2.4.0) @@ -817,9 +808,9 @@ DEPENDENCIES idn-ruby json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 3.0) + json-schema (~> 4.0) kaminari (~> 1.2) - kt-paperclip (~> 7.1)! + kt-paperclip (~> 7.2) letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) @@ -829,9 +820,8 @@ DEPENDENCIES memory_profiler mime-types (~> 3.4.1) net-http (~> 0.3.2) - net-ldap (~> 0.17) - nokogiri (~> 1.14) - nsa (~> 0.2) + net-ldap (~> 0.18) + nokogiri (~> 1.15) oj (~> 3.14) omniauth (~> 1.9) omniauth-cas (~> 2.0) @@ -840,16 +830,15 @@ DEPENDENCIES omniauth_openid_connect (~> 0.6.1) ox (~> 2.14) parslet - pg (~> 1.4) + pg (~> 1.5) pghero - pkg-config (~> 1.5) posix-spawn premailer-rails private_address_check (~> 0.5) public_suffix (~> 5.0) - puma (~> 6.1) + puma (~> 6.3) pundit (~> 2.3) - rack (~> 2.2.6) + rack (~> 2.2.7) rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) @@ -861,17 +850,17 @@ 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 rubocop-rails rubocop-rspec - ruby-progressbar (~> 1.11) + ruby-progressbar (~> 1.13) + rubyzip (~> 2.3) sanitize (~> 6.0) scenic (~> 1.7) sidekiq (~> 6.5) @@ -895,3 +884,9 @@ DEPENDENCIES webpacker (~> 5.4) webpush! xorcist (~> 1.1) + +RUBY VERSION + ruby 3.2.2p53 + +BUNDLED WITH + 2.4.13 diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index e38e14a10..abde8e92f 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/about_controller.rb b/app/controllers/about_controller.rb index 104348614..c4b7e9c9d 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -8,7 +8,7 @@ class AboutController < ApplicationController before_action :set_instance_presenter def show - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 4d03a04b7..929bb54aa 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,8 +7,9 @@ class AccountsController < ApplicationController include AccountControllerConcern include SignatureAuthentication + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -16,7 +17,7 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? @rss_url = rss_url end diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index b8a7e0ab9..388d4b9e1 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -7,10 +7,6 @@ class ActivityPub::BaseController < Api::BaseController private - def set_cache_headers - response.headers['Vary'] = 'Signature' if authorized_fetch_mode? - end - def skip_temporary_suspension_response? false end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 23d874071..4ed59388f 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,11 +4,12 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_items before_action :set_size before_action :set_type - before_action :set_cache_headers def show expires_in 3.minutes, public: public_fetch_mode? diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 4e445bcb1..976caa344 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -4,9 +4,10 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature! before_action :set_items - before_action :set_cache_headers def show expires_in 0, public: false diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 60d201f76..bf10ba762 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -6,9 +6,10 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_statuses - before_action :set_cache_headers def show if page_requested? @@ -16,6 +17,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController else expires_in(3.minutes, public: public_fetch_mode?) end + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end @@ -80,8 +82,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_account @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative end - - def set_cache_headers - response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested? - end end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 8e0f9de2e..c38ff89d1 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -7,9 +7,10 @@ class ActivityPub::RepliesController < ActivityPub::BaseController DESCENDANTS_LIMIT = 60 + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_status - before_action :set_cache_headers before_action :set_replies def index diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb index 351b9a991..8f9708183 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -14,6 +14,10 @@ class Admin::AnnouncementsController < Admin::BaseController @announcement = Announcement.new end + def edit + authorize :announcement, :update? + end + def create authorize :announcement, :create? @@ -28,10 +32,6 @@ class Admin::AnnouncementsController < Admin::BaseController end end - def edit - authorize :announcement, :update? - end - def update authorize :announcement, :update? diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index c645ce12b..a71bb6129 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -9,6 +9,8 @@ module Admin before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers + after_action :verify_authorized private @@ -21,6 +23,10 @@ module Admin use_pack 'admin' end + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + def set_user @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 099512248..3a6df662e 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 750f5c995..b9691c5a3 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 # rubocop:disable Rails/OutputSafety + flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe @domain_block.errors.delete(:domain) - 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/roles_controller.rb b/app/controllers/admin/roles_controller.rb index d76aa745b..bcfc11159 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -16,6 +16,10 @@ module Admin @role = UserRole.new end + def edit + authorize @role, :update? + end + def create authorize :user_role, :create? @@ -30,10 +34,6 @@ module Admin end end - def edit - authorize @role, :update? - end - def update authorize @role, :update? diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb index f3bed3ad8..d31aec6ea 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -11,6 +11,10 @@ module Admin @rule = Rule.new end + def edit + authorize @rule, :update? + end + def create authorize :rule, :create? @@ -24,10 +28,6 @@ module Admin end end - def edit - authorize @rule, :update? - end - def update authorize @rule, :update? diff --git a/app/controllers/admin/warning_presets_controller.rb b/app/controllers/admin/warning_presets_controller.rb index b376f8d9b..efbf65b11 100644 --- a/app/controllers/admin/warning_presets_controller.rb +++ b/app/controllers/admin/warning_presets_controller.rb @@ -11,6 +11,10 @@ module Admin @warning_preset = AccountWarningPreset.new end + def edit + authorize @warning_preset, :update? + end + def create authorize :account_warning_preset, :create? @@ -24,10 +28,6 @@ module Admin end end - def edit - authorize @warning_preset, :update? - end - def update authorize @warning_preset, :update? diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index d6fb1a4ea..f1aad7c4b 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -10,16 +10,25 @@ module Admin @webhooks = Webhook.page(params[:page]) end + def show + authorize @webhook, :show? + end + def new authorize :webhook, :create? @webhook = Webhook.new end + def edit + authorize @webhook, :update? + end + def create authorize :webhook, :create? @webhook = Webhook.new(resource_params) + @webhook.current_account = current_account if @webhook.save redirect_to admin_webhook_path(@webhook) @@ -28,21 +37,15 @@ module Admin end end - def show - authorize @webhook, :show? - end - - def edit - authorize @webhook, :update? - end - 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/base_controller.rb b/app/controllers/api/base_controller.rb index 41f3ce2ee..2629ab782 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -6,13 +6,14 @@ class Api::BaseController < ApplicationController include RateLimitHeaders include AccessTokenTrackingConcern + include ApiCachingConcern - skip_before_action :store_current_location skip_before_action :require_functional!, unless: :whitelist_mode? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_not_suspended! - before_action :set_cache_headers + + vary_by 'Authorization' protect_from_forgery with: :null_session @@ -148,10 +149,6 @@ class Api::BaseController < ApplicationController doorkeeper_authorize!(*scopes) if doorkeeper_token end - def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' - end - def disallow_unauthenticated_api_access? ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 68952de89..1a996d362 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 0a4d2ae7b..6e6ebae43 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb index 8597f891d..6d6339878 100644 --- a/app/controllers/api/v1/accounts/lookup_controller.rb +++ b/app/controllers/api/v1/accounts/lookup_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController before_action :set_account def show + cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 7ed48cf65..51f541bd2 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -7,6 +7,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } def index + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 7dff66efa..ddb94d5ca 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -18,6 +18,7 @@ class Api::V1::AccountsController < Api::BaseController override_rate_limit_headers :follow, family: :follows def show + cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer end @@ -89,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 9ef1b3be7..7b192b979 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 0658199f0..dd54d6710 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -16,19 +16,6 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController PAGINATION_PARAMS = %i(limit).freeze - def create - authorize :domain_allow, :create? - - @domain_allow = DomainAllow.find_by(resource_params) - - if @domain_allow.nil? - @domain_allow = DomainAllow.create!(resource_params) - log_action :create, @domain_allow - end - - render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer - end - def index authorize :domain_allow, :index? render json: @domain_allows, each_serializer: REST::Admin::DomainAllowSerializer @@ -39,6 +26,19 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer end + def create + authorize :domain_allow, :create? + + @domain_allow = DomainAllow.find_by(domain: resource_params[:domain]) + + if @domain_allow.nil? + @domain_allow = DomainAllow.create!(resource_params) + log_action :create, @domain_allow + end + + render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer + end + def destroy authorize @domain_allow, :destroy? UnallowDomainService.new.call(@domain_allow) diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index 8b77e9717..2538c7c7c 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -16,6 +16,16 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController PAGINATION_PARAMS = %i(limit).freeze + def index + authorize :domain_block, :index? + render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer + end + + def show + authorize @domain_block, :show? + render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer + end + def create authorize :domain_block, :create? @@ -28,16 +38,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer end - def index - authorize :domain_block, :index? - render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer - end - - def show - authorize @domain_block, :show? - render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer - end - def update authorize @domain_block, :update? @domain_block.update!(domain_block_params) diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb index e53d0b157..850eda622 100644 --- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -18,15 +18,6 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController limit ).freeze - def create - authorize :email_domain_block, :create? - - @email_domain_block = EmailDomainBlock.create!(resource_params) - log_action :create, @email_domain_block - - render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer - end - def index authorize :email_domain_block, :index? render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer @@ -37,6 +28,15 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer end + def create + authorize :email_domain_block, :create? + + @email_domain_block = EmailDomainBlock.create!(resource_params) + log_action :create, @email_domain_block + + render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer + end + def destroy authorize @email_domain_block, :destroy? @email_domain_block.destroy! diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb index 201ab6b1f..61c191234 100644 --- a/app/controllers/api/v1/admin/ip_blocks_controller.rb +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -18,13 +18,6 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController limit ).freeze - def create - authorize :ip_block, :create? - @ip_block = IpBlock.create!(resource_params) - log_action :create, @ip_block - render json: @ip_block, serializer: REST::Admin::IpBlockSerializer - end - def index authorize :ip_block, :index? render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer @@ -35,6 +28,13 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController render json: @ip_block, serializer: REST::Admin::IpBlockSerializer end + def create + authorize :ip_block, :create? + @ip_block = IpBlock.create!(resource_params) + log_action :create, @ip_block + render json: @ip_block, serializer: REST::Admin::IpBlockSerializer + end + def update authorize @ip_block, :update? @ip_block.update(resource_params) diff --git a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb new file mode 100644 index 000000000..5d9fcc82c --- /dev/null +++ b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseController + include Authorization + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + before_action :set_providers, only: :index + + after_action :verify_authorized + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i(limit).freeze + + def index + authorize :preview_card_provider, :index? + + render json: @providers, each_serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + def approve + authorize :preview_card_provider, :review? + + provider = PreviewCardProvider.find(params[:id]) + provider.update(trendable: true, reviewed_at: Time.now.utc) + render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + def reject + authorize :preview_card_provider, :review? + + provider = PreviewCardProvider.find(params[:id]) + provider.update(trendable: false, reviewed_at: Time.now.utc) + render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + private + + def set_providers + @providers = PreviewCardProvider.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_trends_links_preview_card_providers_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty? + end + + def pagination_max_id + @providers.last.id + end + + def pagination_since_id + @providers.first.id + end + + def records_continue? + @providers.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb index cc6388980..7f4ca4828 100644 --- a/app/controllers/api/v1/admin/trends/links_controller.rb +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::LinksController < Api::V1::Trends::LinksController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index + + def index + if current_user&.can?(:manage_taxonomies) + render json: @links, each_serializer: REST::Admin::Trends::LinkSerializer + else + super + end + end + + def approve + authorize :preview_card, :review? + + link = PreviewCard.find(params[:id]) + link.update(trendable: true) + render json: link, serializer: REST::Admin::Trends::LinkSerializer + end + + def reject + authorize :preview_card, :review? + + link = PreviewCard.find(params[:id]) + link.update(trendable: false) + render json: link, serializer: REST::Admin::Trends::LinkSerializer + end private diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb index c39f77363..34b6580df 100644 --- a/app/controllers/api/v1/admin/trends/statuses_controller.rb +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::StatusesController < Api::V1::Trends::StatusesController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index + + def index + if current_user&.can?(:manage_taxonomies) + render json: @statuses, each_serializer: REST::Admin::Trends::StatusSerializer + else + super + end + end + + def approve + authorize [:admin, :status], :review? + + status = Status.find(params[:id]) + status.update(trendable: true) + render json: status, serializer: REST::Admin::Trends::StatusSerializer + end + + def reject + authorize [:admin, :status], :review? + + status = Status.find(params[:id]) + status.update(trendable: false) + render json: status, serializer: REST::Admin::Trends::StatusSerializer + end private diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index e77df3021..2eeea9522 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index def index if current_user&.can?(:manage_taxonomies) @@ -11,6 +16,22 @@ class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController end end + def approve + authorize :tag, :review? + + tag = Tag.find(params[:id]) + tag.update(trendable: true, reviewed_at: Time.now.utc) + render json: tag, serializer: REST::Admin::TagSerializer + end + + def reject + authorize :tag, :review? + + tag = Tag.find(params[:id]) + tag.update(trendable: false, reviewed_at: Time.now.utc) + render json: tag, serializer: REST::Admin::TagSerializer + end + private def enabled? diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 9034e8a2f..b3ca2f790 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/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 2fad4e4e4..41fee8a67 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class Api::V1::CustomEmojisController < Api::BaseController - skip_before_action :set_cache_headers + vary_by '', unless: :disallow_unauthenticated_api_access? def index - expires_in 3.minutes, public: true + cache_even_if_authenticated! unless disallow_unauthenticated_api_access? render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.enabled.includes(:category) } end end diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c91543e3a..110943550 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -5,6 +5,7 @@ class Api::V1::DirectoriesController < Api::BaseController before_action :set_accounts def show + cache_if_unauthenticated! render json: @accounts, each_serializer: REST::AccountSerializer end @@ -20,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 32fb8e39f..16e91b449 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 edb42a94e..516046f00 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/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index 772791b25..ed98acce3 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -11,6 +11,10 @@ class Api::V1::FiltersController < Api::BaseController render json: @filters, each_serializer: REST::V1::FilterSerializer end + def show + render json: @filter, serializer: REST::V1::FilterSerializer + end + def create ApplicationRecord.transaction do filter_category = current_account.custom_filters.create!(filter_params) @@ -20,10 +24,6 @@ class Api::V1::FiltersController < Api::BaseController render json: @filter, serializer: REST::V1::FilterSerializer end - def show - render json: @filter, serializer: REST::V1::FilterSerializer - end - def update ApplicationRecord.transaction do @filter.update!(keyword_params) diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index bad61425a..3d55d990a 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -3,11 +3,12 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render_with_cache json: :activity, expires_in: 1.day end diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index 37a6906fb..e954c4589 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -6,8 +6,15 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController before_action :require_enabled_api! before_action :set_domain_blocks + vary_by '', if: -> { Setting.show_domain_blocks == 'all' } + def index - expires_in 3.minutes, public: true + if Setting.show_domain_blocks == 'all' + cache_even_if_authenticated! + else + cache_if_unauthenticated! + end + render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) end diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb index c72e16cff..a0665725b 100644 --- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -2,11 +2,19 @@ class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale before_action :set_extended_description + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end + def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer end diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 2877fec52..70281362a 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -3,11 +3,18 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end def index - expires_in 1.day, public: true + cache_even_if_authenticated! render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) } end diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb index dbd69f54d..36889f733 100644 --- a/app/controllers/api/v1/instances/privacy_policies_controller.rb +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController before_action :set_privacy_policy + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer end diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb index 93cf3c759..d3eeca326 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -2,10 +2,19 @@ class Api::V1::Instances::RulesController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale before_action :set_rules + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end + def index + cache_even_if_authenticated! render json: @rules, each_serializer: REST::RuleSerializer end diff --git a/app/controllers/api/v1/instances/translation_languages_controller.rb b/app/controllers/api/v1/instances/translation_languages_controller.rb index 3910a499e..c4680cccb 100644 --- a/app/controllers/api/v1/instances/translation_languages_controller.rb +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Instances::TranslationLanguagesController < Api::BaseController before_action :set_languages + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render json: @languages end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 913319a86..5a6701ff9 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true class Api::V1::InstancesController < Api::BaseController - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 843ca2ec2..4bbbed267 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/media_controller.rb b/app/controllers/api/v1/media_controller.rb index f9c935bf3..5ea26d55b 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -6,19 +6,20 @@ class Api::V1::MediaController < Api::BaseController before_action :set_media_attachment, except: [:create] before_action :check_processing, except: [:create] + def show + render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment + end + def create @media_attachment = current_account.media_attachments.create!(media_attachment_params) render json: @media_attachment, serializer: REST::MediaAttachmentSerializer rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 - rescue Paperclip::Error + rescue Paperclip::Error => e + Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end - def show - render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment - end - def update @media_attachment.update!(updateable_media_attachment_params) render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index 6435e9f0d..ffc70a849 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -8,6 +8,7 @@ class Api::V1::PollsController < Api::BaseController before_action :refresh_poll def show + cache_if_unauthenticated! render json: @poll, serializer: REST::PollSerializer, include_results: true end diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index 7148d63a4..3634acf95 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -6,6 +6,10 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController before_action :set_push_subscription before_action :check_push_subscription, only: [:show, :update] + def show + render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer + end + def create @push_subscription&.destroy! @@ -21,10 +25,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end - def show - render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer - end - def update @push_subscription.update!(data: data_params) render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index b138fa265..73eb11e71 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index 7fe73a6f5..2913472b0 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -7,11 +7,16 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController before_action :set_status def show - render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer + cache_if_unauthenticated! + 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/reactions_controller.rb b/app/controllers/api/v1/statuses/reactions_controller.rb index 333054f2a..2d7e4f598 100644 --- a/app/controllers/api/v1/statuses/reactions_controller.rb +++ b/app/controllers/api/v1/statuses/reactions_controller.rb @@ -9,17 +9,23 @@ class Api::V1::Statuses::ReactionsController < Api::BaseController def create ReactService.new.call(current_account, @status, params[:id]) - render_empty + render json: @status, serializer: REST::StatusSerializer end def destroy - UnreactService.new.call(current_account, @status, params[:id]) - render_empty + UnreactWorker.perform_async(current_account.id, @status.id, params[:id]) + + render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reactions_map: { @status.id => false }) + rescue Mastodon::NotPermittedError + not_found end private def set_status @status = Status.find(params[:status_id]) + authorize @status, :show? + rescue Mastodon::NotPermittedError + not_found end end diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 4b545f982..41672e753 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 1be15a5a4..e3769437b 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/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 8dcf6331e..960f8cf76 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -24,11 +24,14 @@ class Api::V1::StatusesController < Api::BaseController DESCENDANTS_DEPTH_LIMIT = 20 def show + cache_if_unauthenticated! @status = cache_collection([@status], Status).first render json: @status, serializer: REST::StatusSerializer end def context + cache_if_unauthenticated! + ancestors_limit = CONTEXT_LIMIT descendants_limit = CONTEXT_LIMIT descendants_depth_limit = nil diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb index a08fd2187..284ec8593 100644 --- a/app/controllers/api/v1/tags_controller.rb +++ b/app/controllers/api/v1/tags_controller.rb @@ -8,6 +8,7 @@ class Api::V1::TagsController < Api::BaseController override_rate_limit_headers :follow, family: :follows def show + cache_if_unauthenticated! render json: @tag, serializer: REST::TagSerializer end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 493fe4776..6af504ff6 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end @@ -40,7 +41,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController only_media: truthy_param?(:only_media), allow_local_only: truthy_param?(:allow_local_only), with_replies: Setting.show_replies_in_public_timelines, - with_reblogs: Setting.show_reblogs_in_public_timelines, + with_reblogs: Setting.show_reblogs_in_public_timelines ) end diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 64a1db58d..9cd7b9904 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Timelines::TagController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 3ce20fb78..57cfa0b7e 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::Trends::LinksController < Api::BaseController + vary_by 'Authorization, Accept-Language' + before_action :set_links after_action :insert_pagination_headers @@ -8,6 +10,7 @@ class Api::V1::Trends::LinksController < Api::BaseController DEFAULT_LINKS_LIMIT = 10 def index + cache_if_unauthenticated! render json: @links, each_serializer: REST::Trends::LinkSerializer end diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb index 3aab92477..c186864c3 100644 --- a/app/controllers/api/v1/trends/statuses_controller.rb +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true class Api::V1::Trends::StatusesController < Api::BaseController + vary_by 'Authorization, Accept-Language' + before_action :set_statuses after_action :insert_pagination_headers def index + cache_if_unauthenticated! render json: @statuses, each_serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 9dd9abdfe..6cc8194de 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Trends::TagsController < Api::BaseController DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i def index + cache_if_unauthenticated! render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id) end diff --git a/app/controllers/api/v2/admin/accounts_controller.rb b/app/controllers/api/v2/admin/accounts_controller.rb index 0c451f778..65cf0c4db 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/api/v2/filters/keywords_controller.rb b/app/controllers/api/v2/filters/keywords_controller.rb index c63e1d986..fe1a99194 100644 --- a/app/controllers/api/v2/filters/keywords_controller.rb +++ b/app/controllers/api/v2/filters/keywords_controller.rb @@ -12,13 +12,13 @@ class Api::V2::Filters::KeywordsController < Api::BaseController render json: @keywords, each_serializer: REST::FilterKeywordSerializer end - def create - @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params) - + def show render json: @keyword, serializer: REST::FilterKeywordSerializer end - def show + def create + @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params) + render json: @keyword, serializer: REST::FilterKeywordSerializer end diff --git a/app/controllers/api/v2/filters/statuses_controller.rb b/app/controllers/api/v2/filters/statuses_controller.rb index 755c14cff..2e95497a6 100644 --- a/app/controllers/api/v2/filters/statuses_controller.rb +++ b/app/controllers/api/v2/filters/statuses_controller.rb @@ -12,13 +12,13 @@ class Api::V2::Filters::StatusesController < Api::BaseController render json: @status_filters, each_serializer: REST::FilterStatusSerializer end - def create - @status_filter = current_account.custom_filters.find(params[:filter_id]).statuses.create!(resource_params) - + def show render json: @status_filter, serializer: REST::FilterStatusSerializer end - def show + def create + @status_filter = current_account.custom_filters.find(params[:filter_id]).statuses.create!(resource_params) + render json: @status_filter, serializer: REST::FilterStatusSerializer end diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 8ff3076cf..2fcdeeae4 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -11,13 +11,13 @@ class Api::V2::FiltersController < Api::BaseController render json: @filters, each_serializer: REST::FilterSerializer, rules_requested: true end - def create - @filter = current_account.custom_filters.create!(resource_params) - + def show render json: @filter, serializer: REST::FilterSerializer, rules_requested: true end - def show + def create + @filter = current_account.custom_filters.create!(resource_params) + render json: @filter, serializer: REST::FilterSerializer, rules_requested: true end diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index bcd90cff2..8346e2883 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -2,7 +2,7 @@ class Api::V2::InstancesController < Api::V1::InstancesController def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v2/media_controller.rb b/app/controllers/api/v2/media_controller.rb index 288f847f1..72bc69442 100644 --- a/app/controllers/api/v2/media_controller.rb +++ b/app/controllers/api/v2/media_controller.rb @@ -6,7 +6,8 @@ class Api::V2::MediaController < Api::V1::MediaController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200 rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 - rescue Paperclip::Error + rescue Paperclip::Error => e + Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index b084eae42..cc74db58e 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController params[:q], current_account, limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) + search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following)) ) end def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id) + params.permit(:type, :offset, :min_id, :max_id, :account_id, :following) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 906761f6f..7c09040fb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -21,6 +21,8 @@ class ApplicationController < ActionController::Base helper_method :omniauth_only? helper_method :sso_account_settings helper_method :whitelist_mode? + helper_method :body_class_string + helper_method :skip_csrf_meta_tags? rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request rescue_from Mastodon::NotPermittedError, with: :forbidden @@ -37,9 +39,11 @@ class ApplicationController < ActionController::Base service_unavailable end - before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? + before_action :store_referrer, except: :raise_not_found, if: :devise_controller? before_action :require_functional!, if: :user_signed_in? + before_action :set_cache_control_defaults + skip_before_action :verify_authenticity_token, only: :raise_not_found def raise_not_found @@ -56,14 +60,25 @@ class ApplicationController < ActionController::Base !authorized_fetch_mode? end - def store_current_location - store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym) + def store_referrer + return if request.referer.blank? + + redirect_uri = URI(request.referer) + return if redirect_uri.path.start_with?('/auth') + + stored_url = redirect_uri.to_s if redirect_uri.host == request.host && redirect_uri.port == request.port + + store_location_for(:user, stored_url) end def require_functional! redirect_to edit_user_registration_path unless current_user.functional? end + def skip_csrf_meta_tags? + false + end + def after_sign_out_path_for(_resource_or_scope) if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true' '/auth/auth/openid_connect/logout' @@ -127,7 +142,7 @@ class ApplicationController < ActionController::Base end def sso_account_settings - ENV.fetch('SSO_ACCOUNT_SETTINGS') + ENV.fetch('SSO_ACCOUNT_SETTINGS', nil) end def current_account @@ -142,6 +157,10 @@ class ApplicationController < ActionController::Base @current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present? end + def body_class_string + @body_classes || '' + end + def respond_with_error(code) respond_to do |format| format.any do @@ -151,4 +170,8 @@ class ApplicationController < ActionController::Base format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } end end + + def set_cache_control_defaults + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 0817a905c..3283c5f36 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -15,12 +15,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController skip_before_action :require_functional! - def new - super - - resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? - end - def show old_session_values = session.to_hash reset_session @@ -29,6 +23,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController super end + def new + super + + resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? + end + def confirm_captcha check_captcha! do |message| flash.now[:alert] = message @@ -51,14 +51,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController # step. confirmation_token = params[:confirmation_token] return if confirmation_token.nil? + @confirmation_user = User.find_first_by_auth_conditions(confirmation_token: confirmation_token) end 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 @@ -90,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 d2f1bea93..a9d92b6e2 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -25,16 +25,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(&:build_invite_request) end - def destroy - not_found - end - def update super do |resource| resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password? end end + def destroy + not_found + end + protected def update_resource(resource, params) @@ -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 @@ -157,6 +157,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index db5a866f2..8edca4d01 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -11,15 +11,7 @@ class Auth::SetupController < ApplicationController skip_before_action :require_functional! - def show - flash.now[:notice] = begin - if @user.pending? - I18n.t('devise.registrations.signed_up_but_pending') - else - I18n.t('devise.registrations.signed_up_but_unconfirmed') - end - end - end + def show; end def update # This allows updating the e-mail without entering a password as is required @@ -27,14 +19,13 @@ class Auth::SetupController < ApplicationController # that were not confirmed yet if @user.update(user_params) - redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions') + @user.resend_confirmation_instructions unless @user.confirmed? + redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent') else render :show end end - helper_method :missing_email? - private def require_unconfirmed_or_pending! @@ -53,11 +44,7 @@ class Auth::SetupController < ApplicationController params.require(:user).permit(:email) end - def missing_email? - truthy_param?(:missing_email) - end - def set_pack - use_pack 'auth' + use_pack 'sign_up' end end diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index 97fe4a9ab..73f0f2b88 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -60,7 +60,7 @@ class AuthorizeInteractionsController < ApplicationController end def uri_param - params[:uri] || params.fetch(:acct, '').gsub(/\Aacct:/, '') + params[:uri] || params.fetch(:acct, '').delete_prefix('acct:') end def set_body_classes diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 0687b62c5..205df48d4 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(:storage, :fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(Time.now.utc + 10) + if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? + 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/api_caching_concern.rb b/app/controllers/concerns/api_caching_concern.rb new file mode 100644 index 000000000..705abce80 --- /dev/null +++ b/app/controllers/concerns/api_caching_concern.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ApiCachingConcern + extend ActiveSupport::Concern + + def cache_if_unauthenticated! + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + end + + def cache_even_if_authenticated! + expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless whitelist_mode? + end +end diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index a5a9ba3e1..55ebe1bd6 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -155,8 +155,30 @@ module CacheConcern end end + class_methods do + def vary_by(value, **kwargs) + before_action(**kwargs) do |controller| + response.headers['Vary'] = value.respond_to?(:call) ? controller.instance_exec(&value) : value + end + end + end + + included do + after_action :enforce_cache_control! + end + + # Prevents high-entropy headers such as `Cookie`, `Signature` or `Authorization` + # from being used as cache keys, while allowing to `Vary` on them (to not serve + # anonymous cached data to authenticated requests when authentication matters) + def enforce_cache_control! + vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase } + return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? } + + response.cache_control.replace(private: true, no_store: true) + end + def render_with_cache(**options) - raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given? + raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given? key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') expires_in = options.delete(:expires_in) || 3.minutes @@ -176,10 +198,6 @@ module CacheConcern end end - def set_cache_headers - response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature' - end - def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb index 538c1ffb1..576304d1c 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/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 931725943..1d27c92c8 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -180,14 +180,15 @@ module SignatureVerification def build_signed_string signed_headers.map do |signed_header| - if signed_header == Request::REQUEST_TARGET + case signed_header + when Request::REQUEST_TARGET "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" - elsif signed_header == '(created)' + when '(created)' raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? "(created): #{signature_params['created']}" - elsif signed_header == '(expires)' + when '(expires)' raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? @@ -244,7 +245,7 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id) account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) } diff --git a/app/controllers/concerns/theming_concern.rb b/app/controllers/concerns/theming_concern.rb index f993a81d7..82a53dbf5 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/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 7ba7a57e3..96c31566e 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -7,6 +7,12 @@ module WebAppControllerConcern prepend_before_action :redirect_unauthenticated_to_permalinks! before_action :set_pack before_action :set_app_body_class + + vary_by 'Accept, Accept-Language, Cookie' + end + + def skip_csrf_meta_tags? + current_user.nil? end def set_app_body_class diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 9270c467d..e7a02ea89 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,18 +1,8 @@ # frozen_string_literal: true -class CustomCssController < ApplicationController - skip_before_action :store_current_location - skip_before_action :require_functional! - skip_before_action :update_user_sign_in - skip_before_action :set_session_activity - - skip_around_action :set_locale - - before_action :set_cache_headers - +class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController def show expires_in 3.minutes, public: true - request.session_options[:skip] = true render content_type: 'text/css' end end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index 7830c5524..f51f44c62 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -10,6 +10,7 @@ class Disputes::BaseController < ApplicationController before_action :set_body_classes before_action :authenticate_user! before_action :set_pack + before_action :set_cache_headers private @@ -20,4 +21,8 @@ class Disputes::BaseController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb index 41f1e1c5c..72bc56de0 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -2,15 +2,12 @@ class EmojisController < ApplicationController before_action :set_emoji - before_action :set_cache_headers + + vary_by -> { 'Signature' if authorized_fetch_mode? } def show - respond_to do |format| - format.json do - expires_in 3.minutes, public: true - render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter - end - end + expires_in 3.minutes, public: true + render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter end private diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index 86d11fcb9..97206c7ed 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -8,6 +8,7 @@ class Filters::StatusesController < ApplicationController before_action :set_status_filters before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers PER_PAGE = 20 @@ -49,4 +50,8 @@ class Filters::StatusesController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 2ab3b0a74..180ddf070 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -7,6 +7,7 @@ class FiltersController < ApplicationController before_action :set_filter, only: [:edit, :update, :destroy] before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers def index @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @@ -17,6 +18,8 @@ class FiltersController < ApplicationController @filter.keywords.build end + def edit; end + def create @filter = current_account.custom_filters.build(resource_params) @@ -27,8 +30,6 @@ class FiltersController < ApplicationController end end - def edit; end - def update if @filter.update(resource_params) redirect_to filters_path @@ -59,4 +60,8 @@ class FiltersController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 1f5ed30de..2e55cf6c3 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -5,8 +5,9 @@ class FollowerAccountsController < ApplicationController include SignatureVerification include WebAppControllerConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -14,7 +15,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.json do diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index febd13c97..2aa31bdf0 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -5,8 +5,9 @@ class FollowingAccountsController < ApplicationController include SignatureVerification include WebAppControllerConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -14,7 +15,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.json do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d8ee82a7a..ee940e670 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -6,7 +6,7 @@ class HomeController < ApplicationController before_action :set_instance_presenter def index - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 0853897f2..8422d74bc 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true -class InstanceActorsController < ApplicationController - include AccountControllerConcern +class InstanceActorsController < ActivityPub::BaseController + vary_by '' - skip_before_action :check_account_confirmation - skip_around_action :set_locale + serialization_scope nil + + before_action :set_account + skip_before_action :require_functional! + skip_before_action :update_user_sign_in def show expires_in 10.minutes, public: true diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index ca89fc7fe..ea024e30e 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -9,7 +9,7 @@ class IntentsController < ApplicationController if uri.scheme == 'web+mastodon' case uri.host when 'follow' - return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, '')) + return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:')) when 'share' return redirect_to share_path(text: uri.query_values['text']) end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 0b3c082dc..2db4bc5cb 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -8,6 +8,7 @@ class InvitesController < ApplicationController before_action :authenticate_user! before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers def index authorize :invite, :create? @@ -54,4 +55,8 @@ class InvitesController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/mail_subscriptions_controller.rb b/app/controllers/mail_subscriptions_controller.rb new file mode 100644 index 000000000..b071a8060 --- /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/manifests_controller.rb b/app/controllers/manifests_controller.rb index 960510f60..4fba9198f 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -class ManifestsController < ApplicationController - skip_before_action :store_current_location - skip_before_action :require_functional! +class ManifestsController < ActionController::Base # rubocop:disable Rails/ApplicationController + # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` + # and thus re-issuing session cookies + serialization_scope nil def show expires_in 3.minutes, public: true diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 37c5dcb99..ac820e92b 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -3,7 +3,6 @@ class MediaController < ApplicationController include Authorization - skip_before_action :store_current_location skip_before_action :require_functional!, unless: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode? @@ -47,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/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index f29b69a24..8d480d704 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -6,7 +6,6 @@ class MediaProxyController < ApplicationController include Redisable include Lockable - skip_before_action :store_current_location skip_before_action :require_functional! before_action :authenticate_user!, if: :whitelist_mode? @@ -17,7 +16,7 @@ class MediaProxyController < ApplicationController rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show - with_lock("media_download:#{params[:id]}") do + with_redis_lock("media_download:#{params[:id]}") do @media_attachment = MediaAttachment.remote.attached.find(params[:id]) authorize @media_attachment.status, :show? redownload! if @media_attachment.needs_redownload? && !reject_media? diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index d6e7d0800..62fc9c1b0 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -39,6 +39,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index b2564a791..0a1df5506 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -8,6 +8,9 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :set_pack before_action :require_not_suspended!, only: :destroy before_action :set_body_classes + before_action :set_cache_headers + + before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } skip_before_action :require_functional! @@ -35,4 +38,18 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def require_not_suspended! forbidden if current_account.suspended? end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + + 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/privacy_controller.rb b/app/controllers/privacy_controller.rb index 2c98bf3bf..070ee8a06 100644 --- a/app/controllers/privacy_controller.rb +++ b/app/controllers/privacy_controller.rb @@ -8,7 +8,7 @@ class PrivacyController < ApplicationController before_action :set_instance_presenter def show - expires_in 0, public: true if current_account.nil? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 52cf1e0c1..f83098f73 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -8,6 +8,7 @@ class RelationshipsController < ApplicationController before_action :set_pack before_action :set_relationships, only: :show before_action :set_body_classes + before_action :set_cache_headers helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -75,4 +76,8 @@ class RelationshipsController < ApplicationController def set_pack use_pack 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index e6e137c2b..d4b720568 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -8,6 +8,8 @@ class Settings::ApplicationsController < Settings::BaseController @applications = current_user.applications.order(id: :desc).page(params[:page]) end + def show; end + def new @application = Doorkeeper::Application.new( redirect_uri: Doorkeeper.configuration.native_redirect_uri, @@ -15,8 +17,6 @@ class Settings::ApplicationsController < Settings::BaseController ) end - def show; end - def create @application = current_user.applications.build(application_params) diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index bf17b918c..56aeb49aa 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -19,7 +19,7 @@ class Settings::BaseController < ApplicationController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end def require_not_suspended! diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index deaa7940e..46a340aeb 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -15,7 +15,7 @@ class Settings::ExportsController < Settings::BaseController def create backup = nil - with_lock("backup:#{current_user.id}") do + with_redis_lock("backup:#{current_user.id}") do authorize :backup, :create? backup = current_user.backups.create! end diff --git a/app/controllers/settings/flavours_controller.rb b/app/controllers/settings/flavours_controller.rb index c1172598b..b179b9429 100644 --- a/app/controllers/settings/flavours_controller.rb +++ b/app/controllers/settings/flavours_controller.rb @@ -12,9 +12,7 @@ class Settings::FlavoursController < Settings::BaseController end def show - unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) - redirect_to action: 'show', flavour: current_flavour - end + redirect_to action: 'show', flavour: current_flavour unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) @listing = Themes.instance.flavours @selected = params[:flavour] diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index d4516526e..983caf22f 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,31 +1,101 @@ # frozen_string_literal: true -class Settings::ImportsController < Settings::BaseController - before_action :set_account +require 'csv' - def show - @import = Import.new +class Settings::ImportsController < Settings::BaseController + before_action :set_bulk_import, only: [:show, :confirm, :destroy] + before_action :set_recent_imports, only: [:index] + + TYPE_TO_FILENAME_MAP = { + following: 'following_accounts_failures.csv', + blocking: 'blocked_accounts_failures.csv', + muting: 'muted_accounts_failures.csv', + domain_blocking: 'blocked_domains_failures.csv', + bookmarks: 'bookmarks_failures.csv', + lists: 'lists_failures.csv', + }.freeze + + TYPE_TO_HEADERS_MAP = { + following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], + blocking: false, + muting: ['Account address', 'Hide notifications'], + domain_blocking: false, + bookmarks: false, + lists: false, + }.freeze + + def index + @import = Form::Import.new(current_account: current_account) + end + + def show; end + + def failures + @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id]) + + respond_to do |format| + format.csv do + filename = TYPE_TO_FILENAME_MAP[@bulk_import.type.to_sym] + headers = TYPE_TO_HEADERS_MAP[@bulk_import.type.to_sym] + + export_data = CSV.generate(headers: headers, write_headers: true) do |csv| + @bulk_import.rows.find_each do |row| + case @bulk_import.type.to_sym + when :following + csv << [row.data['acct'], row.data.fetch('show_reblogs', true), row.data.fetch('notify', false), row.data['languages']&.join(', ')] + when :blocking + csv << [row.data['acct']] + when :muting + csv << [row.data['acct'], row.data.fetch('hide_notifications', true)] + when :domain_blocking + csv << [row.data['domain']] + when :bookmarks + csv << [row.data['uri']] + when :lists + csv << [row.data['list_name'], row.data['acct']] + end + end + end + + send_data export_data, filename: filename + end + end + end + + def confirm + @bulk_import.update!(state: :scheduled) + BulkImportWorker.perform_async(@bulk_import.id) + redirect_to settings_imports_path, notice: I18n.t('imports.success') end def create - @import = Import.new(import_params) - @import.account = @account + @import = Form::Import.new(import_params.merge(current_account: current_account)) if @import.save - ImportWorker.perform_async(@import.id) - redirect_to settings_import_path, notice: I18n.t('imports.success') + redirect_to settings_import_path(@import.bulk_import.id) else - render :show + # We need to set recent imports as we are displaying the index again + set_recent_imports + render :index end end + def destroy + @bulk_import.destroy! + redirect_to settings_imports_path + end + private - def set_account - @account = current_user.account + def import_params + params.require(:form_import).permit(:data, :type, :mode) end - def import_params - params.require(:import).permit(:data, :type, :mode) + def set_bulk_import + @bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id]) + end + + def set_recent_imports + @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10) end end diff --git a/app/controllers/settings/preferences/appearance_controller.rb b/app/controllers/settings/preferences/appearance_controller.rb index 80ea57bd2..4d7d12bb7 100644 --- a/app/controllers/settings/preferences/appearance_controller.rb +++ b/app/controllers/settings/preferences/appearance_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::AppearanceController < Settings::PreferencesController +class Settings::Preferences::AppearanceController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences/base_controller.rb similarity index 61% rename from app/controllers/settings/preferences_controller.rb rename to app/controllers/settings/preferences/base_controller.rb index 281deb64d..c1f8b4989 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::PreferencesController < Settings::BaseController +class Settings::Preferences::BaseController < Settings::BaseController def show; end def update @@ -15,10 +15,10 @@ class Settings::PreferencesController < Settings::BaseController private def after_update_redirect_path - settings_preferences_path + raise 'Override in controller' 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/preferences/notifications_controller.rb b/app/controllers/settings/preferences/notifications_controller.rb index a16ae6a67..66d6c9a2f 100644 --- a/app/controllers/settings/preferences/notifications_controller.rb +++ b/app/controllers/settings/preferences/notifications_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::NotificationsController < Settings::PreferencesController +class Settings::Preferences::NotificationsController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences/other_controller.rb b/app/controllers/settings/preferences/other_controller.rb index 07eb89a76..a19fbf5c4 100644 --- a/app/controllers/settings/preferences/other_controller.rb +++ b/app/controllers/settings/preferences/other_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::OtherController < Settings::PreferencesController +class Settings::Preferences::OtherController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index cbba842a9..0bff01ec2 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -22,18 +22,9 @@ module Settings private - def confirmation_params - params.require(:form_two_factor_confirmation).permit(:otp_attempt) - end - def verify_otp_not_enabled redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled? end - - def acceptable_code? - current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) || - current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt]) - end end end end diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index 5a9029a42..3f9e71357 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -8,9 +8,8 @@ module Settings before_action :require_otp_enabled before_action :require_webauthn_enabled, only: [:index, :destroy] - def new; end - def index; end + def new; end def options current_user.update(webauthn_id: WebAuthn.generate_user_id) unless current_user.webauthn_id diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb new file mode 100644 index 000000000..fc4f23bb1 --- /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_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 0e7bb835f..3ed1860a0 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -7,6 +7,7 @@ class StatusesCleanupController < ApplicationController before_action :set_policy before_action :set_body_classes before_action :set_pack + before_action :set_cache_headers def show; end @@ -41,4 +42,8 @@ class StatusesCleanupController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 15c081264..0efafb845 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -6,11 +6,12 @@ class StatusesController < ApplicationController include Authorization include AccountOwnedConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter before_action :redirect_to_original, only: :show - before_action :set_cache_headers before_action :set_body_classes, only: :embed after_action :set_link_headers @@ -29,7 +30,7 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? + expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end @@ -45,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/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 4b747c9ad..7e249dbea 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -7,6 +7,8 @@ class TagsController < ApplicationController PAGE_SIZE = 20 PAGE_SIZE_MAX = 200 + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? before_action :set_local @@ -19,7 +21,7 @@ class TagsController < ApplicationController def show respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.rss do diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 2fd6bc7cc..201da9fbc 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true module WellKnown - class HostMetaController < ActionController::Base + class HostMetaController < ActionController::Base # rubocop:disable Rails/ApplicationController include RoutingHelper - before_action { response.headers['Vary'] = 'Accept' } - def show @webfinger_template = "#{webfinger_url}?resource={uri}" expires_in 3.days, public: true diff --git a/app/controllers/well_known/nodeinfo_controller.rb b/app/controllers/well_known/nodeinfo_controller.rb index 11a699ebc..e20e8c62a 100644 --- a/app/controllers/well_known/nodeinfo_controller.rb +++ b/app/controllers/well_known/nodeinfo_controller.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true module WellKnown - class NodeInfoController < ActionController::Base + class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController include CacheConcern - before_action { response.headers['Vary'] = 'Accept' } + # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` + # and thus re-issuing session cookies + serialization_scope nil def index expires_in 3.days, public: true diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 2b296ea3b..0d897e8e2 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module WellKnown - class WebfingerController < ActionController::Base + class WebfingerController < ActionController::Base # rubocop:disable Rails/ApplicationController include RoutingHelper before_action :set_account @@ -18,7 +18,14 @@ module WellKnown private def set_account - @account = Account.find_local!(username_from_resource) + username = username_from_resource + @account = begin + if username == Rails.configuration.x.local_domain + Account.representative + else + Account.find_local!(username) + end + end end def username_from_resource @@ -34,7 +41,12 @@ module WellKnown end def check_account_suspension - expires_in(3.minutes, public: true) && gone if @account.suspended_permanently? + gone if @account.suspended_permanently? + end + + def gone + expires_in(3.minutes, public: true) + head 410 end def bad_request @@ -46,9 +58,5 @@ module WellKnown expires_in(3.minutes, public: true) head 404 end - - def gone - head 410 - end end end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 91c3a116b..b8277ee17 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -28,7 +28,7 @@ module AccountsHelper end def hide_followers_count?(account) - Setting.hide_followers_count || account.user&.settings['hide_followers_count'] + Setting.hide_followers_count || account.user&.settings&.[]('hide_followers_count') end def account_description(account) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2cac2de59..3148756b7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -32,10 +32,6 @@ module ApplicationHelper paths.any? { |path| current_page?(path) } ? 'active' : '' end - def active_link_to(label, path, **options) - link_to label, path, options.merge(class: active_nav_class(path)) - end - def show_landing_strip? !user_signed_in? && !single_user_mode? end @@ -56,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 @@ -117,6 +113,10 @@ module ApplicationHelper content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end + def check_icon + content_tag(:svg, tag.path('fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor') + end + def visibility_icon(status) if status.public_visibility? fa_icon('globe', title: I18n.t('statuses.visibilities.public')) @@ -143,34 +143,22 @@ module ApplicationHelper if prefers_autoplay? image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") else - image_tag(custom_emoji.image.url(:static), class: 'emojione custom-emoji', alt: ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) + image_tag(custom_emoji.image.url(:static), :class => 'emojione custom-emoji', :alt => ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) end end def opengraph(property, content) - tag(:meta, content: content, property: property) - end - - def react_component(name, props = {}, &block) - if block.nil? - content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) }) - else - content_tag(:div, data: { component: name.to_s.camelcase, props: Oj.dump(props) }, &block) - end - end - - def react_admin_component(name, props = {}) - content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) }) + tag.meta(content: content, property: property) end def body_classes - output = (@body_classes || '').split + output = body_class_string.split output << "flavour-#{current_flavour.parameterize}" output << "skin-#{current_skin.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion') output << 'rtl' if locale_direction == 'rtl' - output.reject(&:blank?).join(' ') + output.compact_blank.join(' ') end def cdn_host @@ -182,11 +170,11 @@ module ApplicationHelper end def storage_host - "https://#{ENV['S3_ALIAS_HOST'].presence || ENV['S3_CLOUDFRONT_HOST']}" + "https://#{storage_host_var}" end def storage_host? - ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? + storage_host_var.present? end def quote_wrap(text, line_width: 80, break_sequence: "\n") @@ -244,4 +232,10 @@ module ApplicationHelper def prerender_custom_emojis(html, custom_emojis, other_options = {}) EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s end + + private + + def storage_host_var + ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) + end end diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb index 548c95411..2b9c233c2 100644 --- a/app/helpers/branding_helper.rb +++ b/app/helpers/branding_helper.rb @@ -11,11 +11,11 @@ module BrandingHelper end def _logo_as_symbol_wordmark - content_tag(:svg, tag(:use, href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') + content_tag(:svg, tag.use(href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') end def _logo_as_symbol_icon - content_tag(:svg, tag(:use, href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') + content_tag(:svg, tag.use(href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') end def render_logo diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 5b2ac1a2a..f44cf7973 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -54,6 +54,10 @@ module FormattingHelper end def account_field_value_format(field, with_rel_me: true) - html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) + if field.verified? && !field.account.local? + TextFormatter.shortened_link(field.value_for_verification) + else + html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false) + end end end diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index bedfe6f30..893afdd51 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -9,13 +9,17 @@ module InstanceHelper @site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host end - def description_for_sign_up - prefix = if @invite.present? - I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username) - else - I18n.t('auth.description.prefix_sign_up') - end + def description_for_sign_up(invite = nil) + safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ') + end - safe_join([prefix, I18n.t('auth.description.suffix')], ' ') + private + + def description_prefix(invite) + if invite.present? + I18n.t('auth.description.prefix_invited_by_user', name: invite.user.account.username) + else + I18n.t('auth.description.prefix_sign_up') + end end end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 24362b61e..ce3ff094f 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -63,11 +63,11 @@ module JsonLdHelper uri.nil? || !uri.start_with?('http://', 'https://') end - def invalid_origin?(url) - return true if unsupported_uri_scheme?(url) + def non_matching_uri_hosts?(base_url, comparison_url) + return true if unsupported_uri_scheme?(comparison_url) - needle = Addressable::URI.parse(url).host - haystack = Addressable::URI.parse(@account.uri).host + needle = Addressable::URI.parse(comparison_url).host + haystack = Addressable::URI.parse(base_url).host !haystack.casecmp(needle).zero? end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index bbf0a97fc..840a18d3e 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, @@ -201,7 +199,6 @@ module LanguagesHelper sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze, szl: ['Silesian', 'ślůnsko godka'].freeze, - tai: ['Tai', 'ภาษาไท or ภาษาไต'].freeze, tok: ['Toki Pona', 'toki pona'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, diff --git a/app/helpers/media_component_helper.rb b/app/helpers/media_component_helper.rb new file mode 100644 index 000000000..a57d0b4b6 --- /dev/null +++ b/app/helpers/media_component_helper.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module MediaComponentHelper + def render_video_component(status, **options) + video = status.ordered_media_attachments.first + + meta = video.file.meta || {} + + component_params = { + sensitive: sensitive_viewer?(status, current_account), + src: full_asset_url(video.file.url(:original)), + preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), + alt: video.description, + blurhash: video.blurhash, + frameRate: meta.dig('original', 'frame_rate'), + inline: true, + media: [ + serialize_media_attachment(video), + ].as_json, + }.merge(**options) + + react_component :video, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_audio_component(status, **options) + audio = status.ordered_media_attachments.first + + meta = audio.file.meta || {} + + component_params = { + src: full_asset_url(audio.file.url(:original)), + poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), + alt: audio.description, + backgroundColor: meta.dig('colors', 'background'), + foregroundColor: meta.dig('colors', 'foreground'), + accentColor: meta.dig('colors', 'accent'), + duration: meta.dig('original', 'duration'), + }.merge(**options) + + react_component :audio, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_media_gallery_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + autoplay: prefers_autoplay?, + media: status.ordered_media_attachments.map { |a| serialize_media_attachment(a).as_json }, + }.merge(**options) + + react_component :media_gallery, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_card_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + card: serialize_status_card(status).as_json, + }.merge(**options) + + react_component :card, component_params + end + + def render_poll_component(status, **options) + component_params = { + disabled: true, + poll: serialize_status_poll(status).as_json, + }.merge(**options) + + react_component :poll, component_params do + render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } + end + end + + private + + def serialize_media_attachment(attachment) + ActiveModelSerializers::SerializableResource.new( + attachment, + serializer: REST::MediaAttachmentSerializer + ) + end + + def serialize_status_card(status) + ActiveModelSerializers::SerializableResource.new( + status.preview_card, + serializer: REST::PreviewCardSerializer + ) + end + + def serialize_status_poll(status) + ActiveModelSerializers::SerializableResource.new( + status.preloadable_poll, + serializer: REST::PollSerializer, + scope: current_user, + scope_name: :current_user + ) + end + + def sensitive_viewer?(status, account) + if !account.nil? && account.id == status.account_id + status.sensitive + else + status.account.sensitized? || status.sensitive + end + end +end diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb new file mode 100644 index 000000000..ce616e830 --- /dev/null +++ b/app/helpers/react_component_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ReactComponentHelper + def react_component(name, props = {}, &block) + data = { component: name.to_s.camelcase, props: Oj.dump(props) } + if block.nil? + div_tag_with_data(data) + else + content_tag(:div, data: data, &block) + end + end + + def react_admin_component(name, props = {}) + data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) } + div_tag_with_data(data) + end + + private + + def div_tag_with_data(data) + content_tag(:div, nil, data: data) + end +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 3d5592867..889ca7f40 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/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index d1e3fddaf..f1f1ea872 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -51,14 +51,14 @@ module StatusesHelper end def status_description(status) - components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] + components = [[media_summary(status), status_text_summary(status)].compact_blank.join(' · ')] if status.spoiler_text.blank? components << status.text components << poll_summary(status) end - components.reject(&:blank?).join("\n\n") + components.compact_blank.join("\n\n") end def stream_link_target @@ -105,94 +105,10 @@ module StatusesHelper end end - def sensitized?(status, account) - if !account.nil? && account.id == status.account_id - status.sensitive - else - status.account.sensitized? || status.sensitive - end - end - def embedded_view? params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION end - def render_video_component(status, **options) - video = status.ordered_media_attachments.first - - meta = video.file.meta || {} - - component_params = { - sensitive: sensitized?(status, current_account), - src: full_asset_url(video.file.url(:original)), - preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), - alt: video.description, - blurhash: video.blurhash, - frameRate: meta.dig('original', 'frame_rate'), - inline: true, - media: [ - ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer), - ].as_json, - }.merge(**options) - - react_component :video, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_audio_component(status, **options) - audio = status.ordered_media_attachments.first - - meta = audio.file.meta || {} - - component_params = { - src: full_asset_url(audio.file.url(:original)), - poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), - alt: audio.description, - backgroundColor: meta.dig('colors', 'background'), - foregroundColor: meta.dig('colors', 'foreground'), - accentColor: meta.dig('colors', 'accent'), - duration: meta.dig('original', 'duration'), - }.merge(**options) - - react_component :audio, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_media_gallery_component(status, **options) - component_params = { - sensitive: sensitized?(status, current_account), - autoplay: prefers_autoplay?, - media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }, - }.merge(**options) - - react_component :media_gallery, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_card_component(status, **options) - component_params = { - sensitive: sensitized?(status, current_account), - maxDescription: 160, - card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json, - }.merge(**options) - - react_component :card, component_params - end - - def render_poll_component(status, **options) - component_params = { - disabled: true, - poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json, - }.merge(**options) - - react_component :poll, component_params do - render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } - end - end - def prefers_autoplay? ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index ac1b2f95f..97b2f4e30 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/mailer.js b/app/javascript/core/mailer.js index a4b6d5446..a2ad5e73a 100644 --- a/app/javascript/core/mailer.js +++ b/app/javascript/core/mailer.js @@ -1,3 +1,3 @@ -require('../styles/mailer.scss'); +import '../styles/mailer.scss'; require.context('../icons'); diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js index 5c7a51f44..01b4157f8 100644 --- a/app/javascript/core/public.js +++ b/app/javascript/core/public.js @@ -1,10 +1,8 @@ // This file will be loaded on public pages, regardless of theme. import 'packs/public-path'; -import ready from '../mastodon/ready'; -const { delegate } = require('@rails/ujs'); -const { length } = require('stringz'); +import { delegate } from '@rails/ujs'; const getProfileAvatarAnimationHandler = (swapTo) => { //animate avatar gifs on the profile page when moused over diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index d578463a3..40537377c 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'; -const { delegate } = require('@rails/ujs'); import emojify from '../mastodon/features/emoji/emoji'; diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index b9144e43a..30676dcf5 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 f076cdf30..e76700a48 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 6b5b2ade5..d4f18ff2d 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -1,5 +1,6 @@ import api, { getLinks } from '../api'; -import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; + +import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; @@ -81,7 +82,10 @@ export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST'; export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS'; export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL'; -export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL'; + export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR'; export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE'; @@ -841,6 +845,8 @@ export function fetchPinnedAccountsFail(error) { export function fetchPinnedAccountsSuggestions(q) { return (dispatch, getState) => { + dispatch(fetchPinnedAccountsSuggestionsRequest()); + const params = { q, resolve: false, @@ -850,19 +856,32 @@ export function fetchPinnedAccountsSuggestions(q) { api(getState).get('/api/v1/accounts/search', { params }).then(response => { dispatch(importFetchedAccounts(response.data)); - dispatch(fetchPinnedAccountsSuggestionsReady(q, response.data)); - }); + dispatch(fetchPinnedAccountsSuggestionsSuccess(q, response.data)); + }).catch(err => dispatch(fetchPinnedAccountsSuggestionsFail(err))); }; } -export function fetchPinnedAccountsSuggestionsReady(query, accounts) { +export function fetchPinnedAccountsSuggestionsRequest() { return { - type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY, + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST, + }; +} + +export function fetchPinnedAccountsSuggestionsSuccess(query, accounts) { + return { + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS, query, accounts, }; } +export function fetchPinnedAccountsSuggestionsFail(error) { + return { + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL, + error, + }; +} + export function clearPinnedAccountsSuggestions() { return { type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR, diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index 586dcfd33..339c5f3ad 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.js b/app/javascript/flavours/glitch/actions/app.js deleted file mode 100644 index de2d93e29..000000000 --- a/app/javascript/flavours/glitch/actions/app.js +++ /dev/null @@ -1,6 +0,0 @@ -export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE'; - -export const changeLayout = layout => ({ - type: APP_LAYOUT_CHANGE, - layout, -}); diff --git a/app/javascript/flavours/glitch/actions/app.ts b/app/javascript/flavours/glitch/actions/app.ts new file mode 100644 index 000000000..6fbfc07f6 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/app.ts @@ -0,0 +1,9 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { LayoutType } from '../is_mobile'; + +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 192aa3ce4..e293657ad 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 3c8eec546..0b16f61e6 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 c0f0f3acc..1fc2e391e 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 9c0ef83df..9e0b12370 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'; @@ -413,7 +416,10 @@ export function initMediaEditModal(id) { id, }); - dispatch(openModal('FOCAL_POINT', { id })); + dispatch(openModal({ + modalType: 'FOCAL_POINT', + modalProps: { id }, + })); }; } @@ -441,16 +447,12 @@ export function changeUploadCompose(id, params) { // Editing already-attached media is deferred to editing the post itself. // For simplicity's sake, fake an API reply. if (media && !media.get('unattached')) { - let { description, focus } = params; - const data = media.toJS(); - - if (description) { - data.description = description; - } + const { focus, ...other } = params; + const data = { ...media.toJS(), ...other }; if (focus) { - focus = focus.split(','); - data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } }; + const [x, y] = focus.split(','); + data.meta = { focus: { x: parseFloat(x), y: parseFloat(y) } }; } dispatch(changeUploadComposeSuccess(data, true)); diff --git a/app/javascript/flavours/glitch/actions/conversations.js b/app/javascript/flavours/glitch/actions/conversations.js index 4ef654b1f..8c4c4529f 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 4b2b6dd56..cda63f2b5 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 7388e0c58..2d4d4e620 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 e9c609fc8..a11956ac5 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 c142aaf61..52401b7dc 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 1c9f524e4..540e6cba7 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); @@ -77,7 +78,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); @@ -88,22 +89,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 8f515c990..2a60941cf 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'; @@ -401,7 +402,7 @@ export function unpinFail(status, error) { status, error, }; -}; +} export const addReaction = (statusId, name, url) => (dispatch, getState) => { const status = getState().get('statuses').get(statusId); diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index 5ab922436..b0789cd42 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 adf7fd2ab..f2878daa5 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 dfd701cbb..ccb1b23d6 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'; @@ -55,7 +57,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => { client.open('POST', '/api/v1/markers', false); client.setRequestHeader('Content-Type', 'application/json'); client.setRequestHeader('Authorization', `Bearer ${accessToken}`); - client.SUBMIT(JSON.stringify(params)); + client.send(JSON.stringify(params)); } catch (e) { // Do not make the BeforeUnload handler error out } diff --git a/app/javascript/flavours/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js deleted file mode 100644 index ef2ae0e4c..000000000 --- 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 000000000..af34f5d6a --- /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 aa47d1464..4af927d93 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 ecdac6256..f370905de 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 5038b7eb6..a4a525c42 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/picture_in_picture.js b/app/javascript/flavours/glitch/actions/picture_in_picture.js index 33d8d57d4..898375abe 100644 --- a/app/javascript/flavours/glitch/actions/picture_in_picture.js +++ b/app/javascript/flavours/glitch/actions/picture_in_picture.js @@ -20,9 +20,10 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; * @param {string} accountId * @param {string} playerType * @param {MediaProps} props - * @return {object} + * @returns {object} */ export const deployPictureInPicture = (statusId, accountId, playerType, props) => { + // @ts-expect-error return (dispatch, getState) => { // Do not open a player for a toot that does not exist if (getState().hasIn(['statuses', statusId])) { diff --git a/app/javascript/flavours/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js index d8c0a1373..8aca199e9 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 8e8b82df5..a37410dc9 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 9dcc4bd4b..46b63867f 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 bc5634233..336bbc686 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 fbe5b3791..756b8cd05 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 0012808e5..d5154c6a8 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 091af0f0f..65f3efc3a 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 60f0abf95..120ae133e 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 487cd6988..5bdd31c34 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 137b68e22..e57b37a12 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 ffac1b258..f1c44d2e2 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,22 +24,10 @@ 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 - * @return {number} + * @returns {number} */ const randomUpTo = max => Math.floor(Math.random() * Math.floor(max)); @@ -40,19 +40,24 @@ const randomUpTo = max => * @param {function(Function, Function): void} [options.fallback] * @param {function(): void} [options.fillGaps] * @param {function(object): boolean} [options.accept] - * @return {function(): void} + * @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 let pollingId; /** * @param {function(Function, Function): void} fallback */ + const useFallback = fallback => { fallback(dispatch, () => { + // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); }); }; @@ -61,6 +66,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onConnect() { dispatch(connectTimeline(timelineId)); + // @ts-expect-error if (pollingId) { clearTimeout(pollingId); pollingId = null; @@ -75,6 +81,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti dispatch(disconnectTimeline(timelineId)); if (options.fallback) { + // @ts-expect-error pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000)); } }, @@ -82,24 +89,30 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onReceive (data) { switch(data.event) { case 'update': + // @ts-expect-error dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept)); break; case 'status.update': + // @ts-expect-error dispatch(updateStatus(JSON.parse(data.payload))); break; case 'delete': dispatch(deleteFromTimelines(data.payload)); break; case 'notification': + // @ts-expect-error dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; case 'conversation': + // @ts-expect-error dispatch(updateConversations(JSON.parse(data.payload))); break; case 'announcement': + // @ts-expect-error dispatch(updateAnnouncements(JSON.parse(data.payload))); break; case 'announcement.reaction': + // @ts-expect-error dispatch(updateAnnouncementsReaction(JSON.parse(data.payload))); break; case 'announcement.delete': @@ -109,27 +122,31 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, }; }); +}; /** * @param {Function} dispatch * @param {function(): void} done */ const refreshHomeTimelineAndNotification = (dispatch, done) => { + // @ts-expect-error dispatch(expandHomeTimeline({}, () => + // @ts-expect-error dispatch(expandNotifications({}, () => dispatch(fetchAnnouncements(done)))))); }; /** - * @return {function(): void} + * @returns {function(): void} */ export const connectUserStream = () => + // @ts-expect-error connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); /** * @param {Object} options * @param {boolean} [options.onlyMedia] - * @return {function(): void} + * @returns {function(): void} */ export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) }); @@ -139,7 +156,7 @@ export const connectCommunityStream = ({ onlyMedia } = {}) => * @param {boolean} [options.onlyMedia] * @param {boolean} [options.onlyRemote] * @param {boolean} [options.allowLocalOnly] - * @return {function(): void} + * @returns {function(): void} */ export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) }); @@ -149,20 +166,20 @@ export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = * @param {string} tagName * @param {boolean} onlyLocal * @param {function(object): boolean} accept - * @return {function(): void} + * @returns {function(): void} */ export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) => connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept }); /** - * @return {function(): void} + * @returns {function(): void} */ export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); /** * @param {string} listId - * @return {function(): void} + * @returns {function(): void} */ export const connectListStream = listId => connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) }); diff --git a/app/javascript/flavours/glitch/actions/suggestions.js b/app/javascript/flavours/glitch/actions/suggestions.js index 9e8cd1ea4..870a31102 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 eb817daf9..7d4d56a78 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,10 +1,12 @@ +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'; -import api, { getLinks } from 'flavours/glitch/api'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from 'flavours/glitch/compare_id'; -import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; -import { toServerSideType } from 'flavours/glitch/utils/filters'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -121,7 +123,6 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); @@ -163,10 +164,10 @@ export const expandListTimeline = (id, { maxId } = {}, done = noOp) = export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, - any: parseTags(tags, 'any'), - all: parseTags(tags, 'all'), - none: parseTags(tags, 'none'), - local: local, + any: parseTags(tags, 'any'), + all: parseTags(tags, 'all'), + none: parseTags(tags, 'none'), + local: local, }, done); }; diff --git a/app/javascript/flavours/glitch/actions/trends.js b/app/javascript/flavours/glitch/actions/trends.js index edda0b5b5..d31442388 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 6bbddbef6..948ffbc95 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} @@ -36,7 +36,7 @@ const setCSRFHeader = () => { ready(setCSRFHeader); /** - * @param {() => import('immutable').Map} getState + * @param {() => import('immutable').Map} getState * @returns {import('axios').RawAxiosRequestHeaders} */ const authorizationHeaderFromState = getState => { @@ -52,7 +52,7 @@ const authorizationHeaderFromState = getState => { }; /** - * @param {() => import('immutable').Map} getState + * @param {() => import('immutable').Map} getState * @returns {import('axios').AxiosInstance} */ export default function api(getState) { diff --git a/app/javascript/flavours/glitch/base_polyfills.js b/app/javascript/flavours/glitch/base_polyfills.js deleted file mode 100644 index d3ac0d510..000000000 --- a/app/javascript/flavours/glitch/base_polyfills.js +++ /dev/null @@ -1,42 +0,0 @@ -import 'intl'; -import 'intl/locale-data/jsonp/en'; -import 'es6-symbol/implement'; -import includes from 'array-includes'; -import assign from 'object-assign'; -import values from 'object.values'; -import { decode as decodeBase64 } from './utils/base64'; -import promiseFinally from 'promise.prototype.finally'; - -if (!Array.prototype.includes) { - includes.shim(); -} - -if (!Object.assign) { - Object.assign = assign; -} - -if (!Object.values) { - values.shim(); -} - -promiseFinally.shim(); - -if (!HTMLCanvasElement.prototype.toBlob) { - const BASE64_MARKER = ';base64,'; - - Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { - value(callback, type = 'image/png', quality) { - const dataURL = this.toDataURL(type, quality); - let data; - - if (dataURL.indexOf(BASE64_MARKER) >= 0) { - const [, base64] = dataURL.split(BASE64_MARKER); - data = decodeBase64(base64); - } else { - [, data] = dataURL.split(','); - } - - callback(new Blob([data], { type })); - }, - }); -} diff --git a/app/javascript/flavours/glitch/blurhash.js b/app/javascript/flavours/glitch/blurhash.ts similarity index 81% rename from app/javascript/flavours/glitch/blurhash.js rename to app/javascript/flavours/glitch/blurhash.ts index 5adcc3e77..dadf2b7f2 100644 --- a/app/javascript/flavours/glitch/blurhash.js +++ b/app/javascript/flavours/glitch/blurhash.ts @@ -84,7 +84,7 @@ const DIGIT_CHARACTERS = [ '~', ]; -export const decode83 = (str) => { +export const decode83 = (str: string) => { let value = 0; let c, digit; @@ -97,13 +97,13 @@ export const decode83 = (str) => { return value; }; -export const intToRGB = int => ({ - r: Math.max(0, (int >> 16)), +export const intToRGB = (int: number) => ({ + r: Math.max(0, int >> 16), g: Math.max(0, (int >> 8) & 255), - b: Math.max(0, (int & 255)), + b: Math.max(0, int & 255), }); -export const getAverageFromBlurhash = blurhash => { +export const getAverageFromBlurhash = (blurhash: string) => { if (!blurhash) { return null; } diff --git a/app/javascript/flavours/glitch/compare_id.js b/app/javascript/flavours/glitch/compare_id.ts similarity index 75% rename from app/javascript/flavours/glitch/compare_id.js rename to app/javascript/flavours/glitch/compare_id.ts index d2bd74f44..30b057248 100644 --- a/app/javascript/flavours/glitch/compare_id.js +++ b/app/javascript/flavours/glitch/compare_id.ts @@ -1,4 +1,4 @@ -export default function compareId (id1, id2) { +export function compareId(id1: string, id2: string) { if (id1 === id2) { return 0; } diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx index 7b66d5a6e..518464b04 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 RelativeTimestamp from './relative_timestamp'; -import Skeleton from 'flavours/glitch/components/skeleton'; + +import { Avatar } from './avatar'; +import { DisplayName } from './display_name'; +import { IconButton } from './icon_button'; +import Permalink from './permalink'; +import { RelativeTimestamp } from './relative_timestamp'; + 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 5b6a19f8d..9bb792fc9 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 3dac8c6c2..793fe2dd7 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 000000000..9ec1460fc --- /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 8478ba366..d72465e4a 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, @@ -33,7 +36,7 @@ class Category extends React.PureComponent { const { id, text, disabled, selected, children } = this.props; return ( -
+
{selected && }
@@ -52,7 +55,7 @@ class Category extends React.PureComponent { } -class Rule extends React.PureComponent { +class Rule extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -74,7 +77,7 @@ class Rule extends React.PureComponent { const { id, text, disabled, selected } = this.props; return ( -
+
{selected && } {text} @@ -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 e1ba3f6c9..2cfc30b6f 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 774bf36e6..975ea6e0f 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.jsx b/app/javascript/flavours/glitch/components/animated_number.jsx deleted file mode 100644 index dd21d97f0..000000000 --- a/app/javascript/flavours/glitch/components/animated_number.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ShortNumber from 'mastodon/components/short_number'; -import TransitionMotion from 'react-motion/lib/TransitionMotion'; -import spring from 'react-motion/lib/spring'; -import { reduceMotion } from 'flavours/glitch/initial_state'; - -const obfuscatedCount = count => { - if (count < 0) { - return 0; - } else if (count <= 1) { - return count; - } else { - return '1+'; - } -}; - -export default class AnimatedNumber extends React.PureComponent { - - static propTypes = { - value: PropTypes.number.isRequired, - obfuscate: PropTypes.bool, - }; - - state = { - direction: 1, - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.value > this.props.value) { - this.setState({ direction: 1 }); - } else if (nextProps.value < this.props.value) { - this.setState({ direction: -1 }); - } - } - - willEnter = () => { - const { direction } = this.state; - - return { y: -1 * direction }; - }; - - willLeave = () => { - const { direction } = this.state; - - return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) }; - }; - - render () { - const { value, obfuscate } = this.props; - const { direction } = this.state; - - if (reduceMotion) { - return obfuscate ? obfuscatedCount(value) : ; - } - - const styles = [{ - key: `${value}`, - data: value, - style: { y: spring(0, { damping: 35, stiffness: 400 }) }, - }]; - - return ( - - {items => ( - - {items.map(({ key, data, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } - ))} - - )} - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/animated_number.tsx b/app/javascript/flavours/glitch/components/animated_number.tsx new file mode 100644 index 000000000..81e0af395 --- /dev/null +++ b/app/javascript/flavours/glitch/components/animated_number.tsx @@ -0,0 +1,82 @@ +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; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + +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); + + if (previousValue !== value) { + setPreviousValue(value); + setDirection(value > previousValue ? 1 : -1); + } + + const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]); + const willLeave = useCallback( + () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), + [direction] + ); + + if (reduceMotion) { + return obfuscate ? ( + <>{obfuscatedCount(value)} + ) : ( + + ); + } + + const styles = [ + { + key: `${value}`, + data: value, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }, + ]; + + return ( + + {(items) => ( + + {items.map(({ key, data, style }) => ( + 0 ? 'absolute' : 'static', + transform: `translateY(${style.y * 100}%)`, + }} + > + {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 68b80b19f..173157b0d 100644 --- a/app/javascript/flavours/glitch/components/attachment_list.jsx +++ b/app/javascript/flavours/glitch/components/attachment_list.jsx @@ -1,10 +1,13 @@ -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 Icon from 'flavours/glitch/components/icon'; + +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 83fafbd10..32a996fd7 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 d787ed07a..000000000 --- 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 000000000..932370884 --- /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 90ff298c0..f0833c8c6 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 }); } @@ -180,7 +183,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { } return ( -
+
{inner}
); diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx index 6e6e567b9..25ca3fefa 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 }); } @@ -186,7 +190,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } return ( -
+
{inner}
); diff --git a/app/javascript/flavours/glitch/components/avatar.jsx b/app/javascript/flavours/glitch/components/avatar.jsx deleted file mode 100644 index f30b33e70..000000000 --- a/app/javascript/flavours/glitch/components/avatar.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; -import classNames from 'classnames'; - -export default class Avatar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - className: PropTypes.string, - size: PropTypes.number.isRequired, - style: PropTypes.object, - inline: PropTypes.bool, - animate: PropTypes.bool, - }; - - static defaultProps = { - animate: autoPlayGif, - size: 20, - inline: false, - }; - - state = { - hovering: false, - }; - - handleMouseEnter = () => { - if (this.props.animate) return; - this.setState({ hovering: true }); - }; - - handleMouseLeave = () => { - if (this.props.animate) return; - this.setState({ hovering: false }); - }; - - render () { - const { - account, - animate, - className, - inline, - size, - } = this.props; - const { hovering } = this.state; - - const style = { - ...this.props.style, - width: `${size}px`, - height: `${size}px`, - backgroundSize: `${size}px ${size}px`, - }; - - if (account) { - const src = account.get('avatar'); - const staticSrc = account.get('avatar_static'); - - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; - } - } - - return ( -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/avatar.tsx b/app/javascript/flavours/glitch/components/avatar.tsx new file mode 100644 index 000000000..11253a8e9 --- /dev/null +++ b/app/javascript/flavours/glitch/components/avatar.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; + +import classNames from 'classnames'; + +import { useHovering } from 'flavours/glitch/hooks/useHovering'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; +import type { Account } from 'flavours/glitch/types/resources'; + +interface Props { + account: Account | undefined; + className?: string; + size: number; + style?: React.CSSProperties; + inline?: boolean; +} + +export const Avatar: React.FC = ({ + account, + className, + size = 20, + inline = false, + style: styleFromParent, +}) => { + const { hovering, handleMouseEnter, handleMouseLeave } = + useHovering(autoPlayGif); + + const style = { + ...styleFromParent, + width: `${size}px`, + height: `${size}px`, + backgroundSize: `${size}px ${size}px`, + }; + + if (account) { + style.backgroundImage = `url(${account.get( + hovering ? 'avatar' : 'avatar_static' + )})`; + } + + return ( +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/avatar_composite.jsx b/app/javascript/flavours/glitch/components/avatar_composite.jsx index c0ce7761d..5503abf4a 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, @@ -79,15 +81,7 @@ export default class AvatarComposite extends React.PureComponent { }; return ( - this.props.onAccountClick(account.get('acct'), e)} - title={`@${account.get('acct')}`} - key={account.get('id')} - > -
- +
); } diff --git a/app/javascript/flavours/glitch/components/avatar_overlay.jsx b/app/javascript/flavours/glitch/components/avatar_overlay.jsx index 01dec587a..d8215a478 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.jsx b/app/javascript/flavours/glitch/components/blurhash.jsx deleted file mode 100644 index 2af5cfc56..000000000 --- a/app/javascript/flavours/glitch/components/blurhash.jsx +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-check - -import { decode } from 'blurhash'; -import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; - -/** - * @typedef BlurhashPropsBase - * @property {string?} hash Hash to render - * @property {number} width - * Width of the blurred region in pixels. Defaults to 32 - * @property {number} [height] - * Height of the blurred region in pixels. Defaults to width - * @property {boolean} [dummy] - * Whether dummy mode is enabled. If enabled, nothing is rendered - * and canvas left untouched - */ - -/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */ - -/** - * Component that is used to render blurred of blurhash string - * - * @param {BlurhashProps} param1 Props of the component - * @returns Canvas which will render blurred region element to embed - */ -function Blurhash({ - hash, - width = 32, - height = width, - dummy = false, - ...canvasProps -}) { - const canvasRef = /** @type {import('react').MutableRefObject} */ (useRef()); - - useEffect(() => { - const { current: canvas } = canvasRef; - canvas.width = canvas.width; // resets canvas - - if (dummy || !hash) return; - - try { - const pixels = decode(hash, width, height); - const ctx = canvas.getContext('2d'); - const imageData = new ImageData(pixels, width, height); - - ctx.putImageData(imageData, 0, 0); - } catch (err) { - console.error('Blurhash decoding failure', { err, hash }); - } - }, [dummy, hash, width, height]); - - return ( - - ); -} - -Blurhash.propTypes = { - hash: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - dummy: PropTypes.bool, -}; - -export default React.memo(Blurhash); diff --git a/app/javascript/flavours/glitch/components/blurhash.tsx b/app/javascript/flavours/glitch/components/blurhash.tsx new file mode 100644 index 000000000..d98e7d35d --- /dev/null +++ b/app/javascript/flavours/glitch/components/blurhash.tsx @@ -0,0 +1,49 @@ +import { useRef, useEffect } from 'react'; +import * as React from 'react'; + +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; +} +const Blurhash: React.FC = ({ + hash, + width = 32, + height = width, + dummy = false, + ...canvasProps +}) => { + const canvasRef = useRef(null); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const canvas = canvasRef.current!; + + // eslint-disable-next-line no-self-assign + canvas.width = canvas.width; // resets canvas + + if (dummy || !hash) return; + + try { + const pixels = decode(hash, width, height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(pixels, width, height); + + ctx?.putImageData(imageData, 0, 0); + } catch (err) { + console.error('Blurhash decoding failure', { err, hash }); + } + }, [dummy, hash, width, height]); + + return ( + + ); +}; + +const MemoizedBlurhash = React.memo(Blurhash); + +export { MemoizedBlurhash as Blurhash }; diff --git a/app/javascript/flavours/glitch/components/button.jsx b/app/javascript/flavours/glitch/components/button.jsx index 40b8f5a15..bdeeeac99 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 ee2ef1595..d818480b7 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 000000000..850eb93e4 --- /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 47293ef18..312a6848b 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 e9e2615cb..0934d4b33 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 b43d85b3b..7b3bac45f 100644 --- a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx @@ -1,32 +1,32 @@ -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'; -export default class ColumnBackButtonSlim extends React.PureComponent { +import { FormattedMessage } from 'react-intl'; + +import { Icon } from 'flavours/glitch/components/icon'; + +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('/'); } }; render () { return (
-
+
diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx index 6fbe2955d..e8c056c0b 100644 --- a/app/javascript/flavours/glitch/components/column_header.jsx +++ b/app/javascript/flavours/glitch/components/column_header.jsx @@ -1,9 +1,12 @@ -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 Icon from 'flavours/glitch/components/icon'; + +import classNames from 'classnames'; + +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, @@ -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 dd9b62de9..785907bd2 100644 --- a/app/javascript/flavours/glitch/components/common_counter.jsx +++ b/app/javascript/flavours/glitch/components/common_counter.jsx @@ -1,10 +1,7 @@ // @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 * Type of the counter * @param {boolean} isBold Whether display number must be displayed in bold diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.jsx b/app/javascript/flavours/glitch/components/dismissable_banner.jsx index 9b3faf6f2..21063c9ed 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 19f63ec60..000000000 --- a/app/javascript/flavours/glitch/components/display_name.jsx +++ /dev/null @@ -1,102 +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, - className: PropTypes.string, - inline: PropTypes.bool, - localDomain: PropTypes.string, - others: ImmutablePropTypes.list, - handleClick: PropTypes.func, - }; - - 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 { account, className, inline, localDomain, others, onAccountClick } = this.props; - - const computedClass = classNames('display-name', { inline }, className); - - let displayName, suffix; - let acct; - - if (account) { - acct = account.get('acct'); - - if (acct.indexOf('@') === -1 && localDomain) { - acct = `${acct}@${localDomain}`; - } - } - - if (others && others.size > 0) { - displayName = others.take(2).map(a => ( - onAccountClick(a.get('acct'), e)} - title={`@${a.get('acct')}`} - rel='noopener noreferrer' - > - - - - - )).reduce((prev, cur) => [prev, ', ', cur]); - - if (others.size - 2 > 0) { - displayName.push(` +${others.size - 2}`); - } - - suffix = ( - onAccountClick(account.get('acct'), e)} rel='noopener noreferrer'> - @{acct} - - ); - } else if (account) { - 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 000000000..7224ac3d7 --- /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.jsx b/app/javascript/flavours/glitch/components/domain.jsx deleted file mode 100644 index 85ebdbde9..000000000 --- a/app/javascript/flavours/glitch/components/domain.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, -}); - -class Account extends ImmutablePureComponent { - - static propTypes = { - domain: PropTypes.string, - onUnblockDomain: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleDomainUnblock = () => { - this.props.onUnblockDomain(this.props.domain); - }; - - render () { - const { domain, intl } = this.props; - - return ( -
-
- - {domain} - - -
- -
-
-
- ); - } - -} - -export default injectIntl(Account); diff --git a/app/javascript/flavours/glitch/components/domain.tsx b/app/javascript/flavours/glitch/components/domain.tsx new file mode 100644 index 000000000..50c5c256e --- /dev/null +++ b/app/javascript/flavours/glitch/components/domain.tsx @@ -0,0 +1,45 @@ +import { useCallback } from 'react'; +import * as React from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { IconButton } from './icon_button'; + +const messages = defineMessages({ + unblockDomain: { + id: 'account.unblock_domain', + defaultMessage: 'Unblock domain {domain}', + }, +}); + +interface Props { + domain: string; + onUnblockDomain: (domain: string) => void; +} + +export const Domain: React.FC = ({ domain, onUnblockDomain }) => { + const intl = useIntl(); + + const handleDomainUnblock = useCallback(() => { + onUnblockDomain(domain); + }, [domain, onUnblockDomain]); + + return ( +
+
+ + {domain} + + +
+ +
+
+
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx index f4b6e059f..0416df5d4 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,11 +120,11 @@ 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 a1519757d..7c9c16713 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 6d73fa68c..3dbac58b5 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, @@ -32,7 +39,7 @@ class EditedTimestamp extends React.PureComponent { renderHeader = items => { return ( - + ); }; diff --git a/app/javascript/flavours/glitch/components/error_boundary.jsx b/app/javascript/flavours/glitch/components/error_boundary.jsx index 8518dfc86..4a4dadf0a 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, @@ -72,7 +76,7 @@ export default class ErrorBoundary extends React.PureComponent { } return ( -
    +

    diff --git a/app/javascript/flavours/glitch/components/gifv.jsx b/app/javascript/flavours/glitch/components/gifv.jsx deleted file mode 100644 index 9ec201c6c..000000000 --- a/app/javascript/flavours/glitch/components/gifv.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class GIFV extends React.PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string, - lang: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - onClick: PropTypes.func, - }; - - state = { - loading: true, - }; - - handleLoadedData = () => { - this.setState({ loading: false }); - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.src !== this.props.src) { - this.setState({ loading: true }); - } - } - - handleClick = e => { - const { onClick } = this.props; - - if (onClick) { - e.stopPropagation(); - onClick(); - } - }; - - render () { - const { src, width, height, alt, lang } = this.props; - const { loading } = this.state; - - return ( -

    - {loading && ( - - )} - -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/gifv.tsx b/app/javascript/flavours/glitch/components/gifv.tsx new file mode 100644 index 000000000..c9b9de29b --- /dev/null +++ b/app/javascript/flavours/glitch/components/gifv.tsx @@ -0,0 +1,71 @@ +import { useCallback, useState } from 'react'; +import * as React from 'react'; + +interface Props { + src: string; + key: string; + alt?: string; + lang?: string; + width: number; + height: number; + onClick?: () => void; +} + +export const GIFV: React.FC = ({ + src, + alt, + lang, + width, + height, + onClick, +}) => { + const [loading, setLoading] = useState(true); + + const handleLoadedData: React.ReactEventHandler = + useCallback(() => { + setLoading(false); + }, [setLoading]); + + const handleClick: React.MouseEventHandler = useCallback( + (e) => { + if (onClick) { + e.stopPropagation(); + onClick(); + } + }, + [onClick] + ); + + return ( +
    + {loading && ( + + )} + +
    + ); +}; diff --git a/app/javascript/flavours/glitch/components/hashtag.jsx b/app/javascript/flavours/glitch/components/hashtag.jsx index 422b9a8fa..422ead01d 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, @@ -35,13 +41,12 @@ class SilentErrorBoundary extends React.Component { /** * Used to render counter of how much people are talking about hashtag - * * @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} */ export const accountsCountRenderer = (displayNumber, pluralReady) => ( {displayNumber}
    , @@ -50,12 +55,14 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => ( /> ); +// @ts-expect-error export const ImmutableHashtag = ({ hashtag }) => ( day.get('uses')).toArray()} /> ); @@ -64,11 +71,12 @@ ImmutableHashtag.propTypes = { hashtag: ImmutablePropTypes.map.isRequired, }; +// @ts-expect-error const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
    - {name ? #{name} : } + {name ? <>#{name} : } {description ? ( diff --git a/app/javascript/flavours/glitch/components/icon.jsx b/app/javascript/flavours/glitch/components/icon.jsx deleted file mode 100644 index d8a17722f..000000000 --- a/app/javascript/flavours/glitch/components/icon.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class Icon extends React.PureComponent { - - static propTypes = { - id: PropTypes.string.isRequired, - className: PropTypes.string, - fixedWidth: PropTypes.bool, - }; - - render () { - const { id, className, fixedWidth, ...other } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/icon.tsx b/app/javascript/flavours/glitch/components/icon.tsx new file mode 100644 index 000000000..765aa89ae --- /dev/null +++ b/app/javascript/flavours/glitch/components/icon.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; + +import classNames from 'classnames'; + +interface Props extends React.HTMLAttributes { + id: string; + className?: string; + fixedWidth?: boolean; + children?: never; +} + +export const Icon: React.FC = ({ + id, + className, + fixedWidth, + ...other +}) => ( + +); diff --git a/app/javascript/flavours/glitch/components/icon_button.jsx b/app/javascript/flavours/glitch/components/icon_button.tsx similarity index 60% rename from app/javascript/flavours/glitch/components/icon_button.jsx rename to app/javascript/flavours/glitch/components/icon_button.tsx index 10d7926be..ecc418773 100644 --- a/app/javascript/flavours/glitch/components/icon_button.jsx +++ b/app/javascript/flavours/glitch/components/icon_button.tsx @@ -1,45 +1,46 @@ -import React from 'react'; -import Motion from '../features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import PropTypes from 'prop-types'; +import * as React from 'react'; + import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import AnimatedNumber from 'flavours/glitch/components/animated_number'; -export default class IconButton extends React.PureComponent { - - static propTypes = { - className: PropTypes.string, - title: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - onClick: PropTypes.func, - onMouseDown: PropTypes.func, - onKeyDown: PropTypes.func, - onKeyPress: PropTypes.func, - size: PropTypes.number, - active: PropTypes.bool, - expanded: PropTypes.bool, - style: PropTypes.object, - activeStyle: PropTypes.object, - disabled: PropTypes.bool, - inverted: PropTypes.bool, - animate: PropTypes.bool, - overlay: PropTypes.bool, - tabIndex: PropTypes.string, - label: PropTypes.string, - counter: PropTypes.number, - obfuscateCount: PropTypes.bool, - href: PropTypes.string, - ariaHidden: PropTypes.bool, - }; +import { AnimatedNumber } from './animated_number'; +import { Icon } from './icon'; +interface Props { + className?: string; + title: string; + icon: string; + onClick?: React.MouseEventHandler; + onMouseDown?: React.MouseEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onKeyPress?: React.KeyboardEventHandler; + size: number; + active: boolean; + expanded?: boolean; + style?: React.CSSProperties; + activeStyle?: React.CSSProperties; + disabled: boolean; + inverted?: boolean; + animate: boolean; + overlay: boolean; + tabIndex: number; + label?: string; + counter?: number; + obfuscateCount?: boolean; + href?: string; + ariaHidden: boolean; +} +interface States { + activate: boolean; + deactivate: boolean; +} +export class IconButton extends React.PureComponent { static defaultProps = { size: 18, active: false, disabled: false, animate: false, overlay: false, - tabIndex: '0', + tabIndex: 0, ariaHidden: false, }; @@ -48,7 +49,7 @@ export default class IconButton extends React.PureComponent { deactivate: false, }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { if (!nextProps.animate) return; if (this.props.active && !nextProps.active) { @@ -58,40 +59,40 @@ export default class IconButton extends React.PureComponent { } } - handleClick = (e) => { + handleClick: React.MouseEventHandler = (e) => { e.preventDefault(); - if (!this.props.disabled) { + if (!this.props.disabled && this.props.onClick != null) { this.props.onClick(e); } }; - handleKeyPress = (e) => { + handleKeyPress: React.KeyboardEventHandler = (e) => { if (this.props.onKeyPress && !this.props.disabled) { this.props.onKeyPress(e); } }; - handleMouseDown = (e) => { + handleMouseDown: React.MouseEventHandler = (e) => { if (!this.props.disabled && this.props.onMouseDown) { this.props.onMouseDown(e); } }; - handleKeyDown = (e) => { + handleKeyDown: React.KeyboardEventHandler = (e) => { if (!this.props.disabled && this.props.onKeyDown) { this.props.onKeyDown(e); } }; - render () { + render() { // Hack required for some icons which have an overriden size let containerSize = '1.28571429em'; if (this.props.style?.fontSize) { containerSize = `${this.props.size * 1.28571429}px`; } - let style = { + const style = { fontSize: `${this.props.size}px`, height: containerSize, lineHeight: `${this.props.size}px`, @@ -120,10 +121,7 @@ export default class IconButton extends React.PureComponent { ariaHidden, } = this.props; - const { - activate, - deactivate, - } = this.state; + const { activate, deactivate } = this.state; const classes = classNames(className, 'icon-button', { active, @@ -140,13 +138,18 @@ export default class IconButton extends React.PureComponent { } let contents = ( - - + ); - if (href && !this.prop) { + if (href != null) { contents = ( {contents} @@ -156,6 +159,7 @@ export default class IconButton extends React.PureComponent { 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 000000000..c86e70f9f --- /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 ab9428e35..000000000 --- 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 000000000..8b5746ad3 --- /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 a75259146..000000000 --- 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 000000000..f7589622e --- /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 59f721c50..000000000 --- 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 000000000..6bc24a0d6 --- /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 ee5c22496..16ca9f80f 100644 --- a/app/javascript/flavours/glitch/components/logo.jsx +++ b/app/javascript/flavours/glitch/components/logo.jsx @@ -1,10 +1,14 @@ -import React from 'react'; +import logo from 'mastodon/../images/logo.svg'; -const Logo = () => ( - +export const WordmarkLogo = () => ( + Mastodon ); -export default Logo; +export const SymbolLogo = () => ( + Mastodon +); + +export default WordmarkLogo; diff --git a/app/javascript/flavours/glitch/components/media_attachments.jsx b/app/javascript/flavours/glitch/components/media_attachments.jsx index b11d3526f..4e777437a 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 => ( 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } + if (attachment.get('description')?.length > 0) { + badges.push(ALT); } - let thumbnail = ''; + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); if (attachment.get('type') === 'unknown') { return ( -
    - +
    + GIF); + thumbnail = (
    ); } return ( -
    +
    + {visible && thumbnail} + + {badges && ( +
    + {badges} +
    + )}
    ); } } -class MediaGallery extends React.PureComponent { +class MediaGallery extends PureComponent { static propTypes = { sensitive: PropTypes.bool, @@ -281,7 +262,7 @@ class MediaGallery extends React.PureComponent { window.removeEventListener('resize', this.handleResize); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) { this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' }); } else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { @@ -289,7 +270,7 @@ class MediaGallery extends React.PureComponent { } } - componentDidUpdate (prevProps) { + componentDidUpdate () { if (this.node) { this.handleResize(); } @@ -313,7 +294,7 @@ class MediaGallery extends React.PureComponent { }; handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index); + this.props.onOpenMedia(this.props.media, index, this.props.lang); }; handleRef = (node) => { @@ -327,7 +308,7 @@ class MediaGallery extends React.PureComponent { _setDimensions () { const width = this.node.offsetWidth; - if (width && width != this.state.width) { + if (width && width !== this.state.width) { // offsetWidth triggers a layout, so only calculate when we need to if (this.props.cacheWidth) { this.props.cacheWidth(width); @@ -358,12 +339,10 @@ class MediaGallery extends React.PureComponent { const computedClass = classNames('media-gallery', { 'full-width': fullwidth }); - if (this.isStandaloneEligible() && width) { - style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); - } else if (width) { - style.height = width / (16/9); + if (this.isStandaloneEligible()) { // TODO: cropImages setting + style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`; } else { - return (
    ); + style.aspectRatio = '16 / 9'; } if (this.isStandaloneEligible()) { diff --git a/app/javascript/flavours/glitch/components/missing_indicator.jsx b/app/javascript/flavours/glitch/components/missing_indicator.jsx deleted file mode 100644 index 08e39c236..000000000 --- a/app/javascript/flavours/glitch/components/missing_indicator.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import illustration from 'flavours/glitch/images/elephant_ui_disappointed.svg'; -import classNames from 'classnames'; -import { Helmet } from 'react-helmet'; - -const MissingIndicator = ({ fullPage }) => ( -
    -
    - -
    - -
    - - -
    - - - - -
    -); - -MissingIndicator.propTypes = { - fullPage: PropTypes.bool, -}; - -export default MissingIndicator; diff --git a/app/javascript/flavours/glitch/components/modal_root.jsx b/app/javascript/flavours/glitch/components/modal_root.jsx index 5a5563e87..a99c51f92 100644 --- a/app/javascript/flavours/glitch/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/components/modal_root.jsx @@ -1,10 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import 'wicg-inert'; -import { createBrowserHistory } from 'history'; -import { multiply } from 'color-blend'; +import { PureComponent } from 'react'; -export default class ModalRoot extends React.PureComponent { +import 'wicg-inert'; +import { multiply } from 'color-blend'; +import { createBrowserHistory } from 'history'; + +export default class ModalRoot extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -62,7 +63,7 @@ export default class ModalRoot extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!!nextProps.children && !this.props.children) { this.activeElement = document.activeElement; diff --git a/app/javascript/flavours/glitch/components/navigation_portal.jsx b/app/javascript/flavours/glitch/components/navigation_portal.jsx index 9e8494179..e142a3ec6 100644 --- a/app/javascript/flavours/glitch/components/navigation_portal.jsx +++ b/app/javascript/flavours/glitch/components/navigation_portal.jsx @@ -1,21 +1,21 @@ -import React from 'react'; +import { PureComponent } from 'react'; + import { Switch, Route, withRouter } from 'react-router-dom'; -import { showTrends } from 'flavours/glitch/initial_state'; -import Trends from 'flavours/glitch/features/getting_started/containers/trends_container'; + import AccountNavigation from 'flavours/glitch/features/account/navigation'; +import Trends from 'flavours/glitch/features/getting_started/containers/trends_container'; +import { showTrends } from 'flavours/glitch/initial_state'; const DefaultNavigation = () => ( - <> - {showTrends && ( - <> -
    - - - )} - + 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.jsx b/app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx deleted file mode 100644 index b440c6be2..000000000 --- a/app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -const NotSignedInIndicator = () => ( -
    -
    - -
    -
    -); - -export default NotSignedInIndicator; diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx new file mode 100644 index 000000000..d0eedc641 --- /dev/null +++ b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +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 1d807bc23..dfc7ac5a8 100644 --- a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx +++ b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx @@ -6,11 +6,15 @@ // Package imports // -import React from 'react'; import PropTypes from 'prop-types'; + import { defineMessages, injectIntl } from 'react-intl'; + +import classNames from 'classnames'; + import ImmutablePureComponent from 'react-immutable-pure-component'; -import Icon from 'flavours/glitch/components/icon'; + +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' }, @@ -36,19 +40,19 @@ class NotificationPurgeButtons extends ImmutablePureComponent { //className='active' return (
    - - - -
    diff --git a/app/javascript/flavours/glitch/components/permalink.jsx b/app/javascript/flavours/glitch/components/permalink.jsx index b09b17eeb..fa33ce066 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 961d3dead..1a290c91d 100644 --- a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx @@ -1,64 +1,27 @@ -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 { debounce } from 'lodash'; +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 = { - width: PropTypes.number, dispatch: PropTypes.func.isRequired, }; - state = { - width: this.props.width, - height: this.props.width && (this.props.width / (16/9)), - }; - handleClick = () => { const { dispatch } = this.props; dispatch(removePictureInPicture()); }; - setRef = c => { - this.node = c; - - if (this.node) { - this._setDimensions(); - } - }; - - _setDimensions () { - const width = this.node.offsetWidth; - const height = width / (16/9); - - this.setState({ width, height }); - } - - componentDidMount () { - window.addEventListener('resize', this.handleResize, { passive: true }); - } - - componentWillUnmount () { - window.removeEventListener('resize', this.handleResize); - } - - handleResize = debounce(() => { - if (this.node) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - render () { - const { height } = this.state; - return ( -
    +
    diff --git a/app/javascript/flavours/glitch/components/poll.jsx b/app/javascript/flavours/glitch/components/poll.jsx index 2ccc1761e..623d34380 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 spring from 'react-motion/lib/spring'; + +import { Icon } from 'flavours/glitch/components/icon'; import emojify from 'flavours/glitch/features/emoji/emoji'; -import RelativeTimestamp from './relative_timestamp'; -import Icon from 'flavours/glitch/components/icon'; +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); @@ -125,6 +131,10 @@ class Poll extends ImmutablePureComponent { this.props.refresh(); }; + handleReveal = () => { + this.setState({ revealed: true }); + } + renderOption (option, optionIndex, showResults) { const { poll, lang, disabled, intl } = this.props; const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); @@ -133,10 +143,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 ( @@ -154,11 +166,11 @@ class Poll extends ImmutablePureComponent { {!showResults && ( @@ -177,7 +189,7 @@ class Poll extends ImmutablePureComponent { {!!voted && @@ -198,14 +210,14 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; - const { expired } = this.state; + const { revealed, expired } = this.state; if (!poll) { return null; } const timeRemaining = expired ? intl.formatMessage(messages.closed) : ; - const showResults = poll.get('voted') || expired; + const showResults = poll.get('voted') || revealed || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); let votesCount = null; @@ -224,9 +236,10 @@ class Poll extends ImmutablePureComponent {
    {!showResults && } - {showResults && !this.props.disabled && · } + {!showResults && <> · } + {showResults && !this.props.disabled && <> · } {votesCount} - {poll.get('expires_at') && · {timeRemaining}} + {poll.get('expires_at') && <> · {timeRemaining}}
    ); diff --git a/app/javascript/flavours/glitch/components/radio_button.jsx b/app/javascript/flavours/glitch/components/radio_button.jsx deleted file mode 100644 index 0496fa286..000000000 --- a/app/javascript/flavours/glitch/components/radio_button.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class RadioButton extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - checked: PropTypes.bool, - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - label: PropTypes.node.isRequired, - }; - - render () { - const { name, value, checked, onChange, label } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/radio_button.tsx b/app/javascript/flavours/glitch/components/radio_button.tsx new file mode 100644 index 000000000..fbd7859d8 --- /dev/null +++ b/app/javascript/flavours/glitch/components/radio_button.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; + +import classNames from 'classnames'; + +interface Props { + value: string; + checked: boolean; + name: string; + onChange: (event: React.ChangeEvent) => void; + label: React.ReactNode; +} + +export const RadioButton: React.FC = ({ + name, + value, + checked, + onChange, + label, +}) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/regeneration_indicator.jsx b/app/javascript/flavours/glitch/components/regeneration_indicator.jsx index 68ce09df9..78844f389 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.jsx b/app/javascript/flavours/glitch/components/relative_timestamp.jsx deleted file mode 100644 index e6c3e0880..000000000 --- a/app/javascript/flavours/glitch/components/relative_timestamp.jsx +++ /dev/null @@ -1,200 +0,0 @@ -import React from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; -import PropTypes from 'prop-types'; - -const messages = defineMessages({ - today: { id: 'relative_time.today', defaultMessage: 'today' }, - just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, - just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' }, - seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, - seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' }, - minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, - minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' }, - hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, - hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' }, - days: { id: 'relative_time.days', defaultMessage: '{number}d' }, - days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' }, - moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, - seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, - minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, - hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, - days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, -}); - -const dateFormatOptions = { - hour12: false, - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', -}; - -const shortDateFormatOptions = { - month: 'short', - day: 'numeric', -}; - -const SECOND = 1000; -const MINUTE = 1000 * 60; -const HOUR = 1000 * 60 * 60; -const DAY = 1000 * 60 * 60 * 24; - -const MAX_DELAY = 2147483647; - -const selectUnits = delta => { - const absDelta = Math.abs(delta); - - if (absDelta < MINUTE) { - return 'second'; - } else if (absDelta < HOUR) { - return 'minute'; - } else if (absDelta < DAY) { - return 'hour'; - } - - return 'day'; -}; - -const getUnitDelay = units => { - switch (units) { - case 'second': - return SECOND; - case 'minute': - return MINUTE; - case 'hour': - return HOUR; - case 'day': - return DAY; - default: - return MAX_DELAY; - } -}; - -export const timeAgoString = (intl, date, now, year, timeGiven, short) => { - const delta = now - date.getTime(); - - let relativeTime; - - if (delta < DAY && !timeGiven) { - relativeTime = intl.formatMessage(messages.today); - } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full); - } else if (delta < 7 * DAY) { - if (delta < MINUTE) { - relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) }); - } else if (delta < HOUR) { - relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) }); - } else if (delta < DAY) { - relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) }); - } else { - relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) }); - } - } else if (date.getFullYear() === year) { - relativeTime = intl.formatDate(date, shortDateFormatOptions); - } else { - relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' }); - } - - return relativeTime; -}; - -const timeRemainingString = (intl, date, now, timeGiven = true) => { - const delta = date.getTime() - now; - - let relativeTime; - - if (delta < DAY && !timeGiven) { - relativeTime = intl.formatMessage(messages.today); - } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(messages.moments_remaining); - } else if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); - } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) }); - } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) }); - } else { - relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) }); - } - - return relativeTime; -}; - -class RelativeTimestamp extends React.Component { - - static propTypes = { - intl: PropTypes.object.isRequired, - timestamp: PropTypes.string.isRequired, - year: PropTypes.number.isRequired, - futureDate: PropTypes.bool, - short: PropTypes.bool, - }; - - state = { - now: this.props.intl.now(), - }; - - static defaultProps = { - year: (new Date()).getFullYear(), - short: true, - }; - - shouldComponentUpdate (nextProps, nextState) { - // As of right now the locale doesn't change without a new page load, - // but we might as well check in case that ever changes. - return this.props.timestamp !== nextProps.timestamp || - this.props.intl.locale !== nextProps.intl.locale || - this.state.now !== nextState.now; - } - - componentWillReceiveProps (nextProps) { - if (this.props.timestamp !== nextProps.timestamp) { - this.setState({ now: this.props.intl.now() }); - } - } - - componentDidMount () { - this._scheduleNextUpdate(this.props, this.state); - } - - componentWillUpdate (nextProps, nextState) { - this._scheduleNextUpdate(nextProps, nextState); - } - - componentWillUnmount () { - clearTimeout(this._timer); - } - - _scheduleNextUpdate (props, state) { - clearTimeout(this._timer); - - const { timestamp } = props; - const delta = (new Date(timestamp)).getTime() - state.now; - const unitDelay = getUnitDelay(selectUnits(delta)); - const unitRemainder = Math.abs(delta % unitDelay); - const updateInterval = 1000 * 10; - const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder); - - this._timer = setTimeout(() => { - this.setState({ now: this.props.intl.now() }); - }, delay); - } - - render () { - const { timestamp, intl, year, futureDate, short } = this.props; - - const timeGiven = timestamp.includes('T'); - const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short); - - return ( - - ); - } - -} - -export default injectIntl(RelativeTimestamp); diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.tsx b/app/javascript/flavours/glitch/components/relative_timestamp.tsx new file mode 100644 index 000000000..e4a8437d0 --- /dev/null +++ b/app/javascript/flavours/glitch/components/relative_timestamp.tsx @@ -0,0 +1,282 @@ +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' }, + just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, + just_now_full: { + id: 'relative_time.full.just_now', + defaultMessage: 'just now', + }, + seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, + seconds_full: { + id: 'relative_time.full.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} ago', + }, + minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, + minutes_full: { + id: 'relative_time.full.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago', + }, + hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, + hours_full: { + id: 'relative_time.full.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} ago', + }, + days: { id: 'relative_time.days', defaultMessage: '{number}d' }, + days_full: { + id: 'relative_time.full.days', + defaultMessage: '{number, plural, one {# day} other {# days}} ago', + }, + moments_remaining: { + id: 'time_remaining.moments', + defaultMessage: 'Moments remaining', + }, + seconds_remaining: { + id: 'time_remaining.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} left', + }, + minutes_remaining: { + id: 'time_remaining.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} left', + }, + hours_remaining: { + id: 'time_remaining.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} left', + }, + days_remaining: { + id: 'time_remaining.days', + defaultMessage: '{number, plural, one {# day} other {# days}} left', + }, +}); + +const dateFormatOptions = { + hour12: false, + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', +} as const; + +const shortDateFormatOptions = { + month: 'short', + day: 'numeric', +} as const; + +const SECOND = 1000; +const MINUTE = 1000 * 60; +const HOUR = 1000 * 60 * 60; +const DAY = 1000 * 60 * 60 * 24; + +const MAX_DELAY = 2147483647; + +const selectUnits = (delta: number) => { + const absDelta = Math.abs(delta); + + if (absDelta < MINUTE) { + return 'second'; + } else if (absDelta < HOUR) { + return 'minute'; + } else if (absDelta < DAY) { + return 'hour'; + } + + return 'day'; +}; + +const getUnitDelay = (units: string) => { + switch (units) { + case 'second': + return SECOND; + case 'minute': + return MINUTE; + case 'hour': + return HOUR; + case 'day': + return DAY; + default: + return MAX_DELAY; + } +}; + +export const timeAgoString = ( + intl: IntlShape, + date: Date, + now: number, + year: number, + timeGiven: boolean, + short?: boolean +) => { + const delta = now - date.getTime(); + + let relativeTime; + + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { + relativeTime = intl.formatMessage( + short ? messages.just_now : messages.just_now_full + ); + } else if (delta < 7 * DAY) { + if (delta < MINUTE) { + relativeTime = intl.formatMessage( + short ? messages.seconds : messages.seconds_full, + { number: Math.floor(delta / SECOND) } + ); + } else if (delta < HOUR) { + relativeTime = intl.formatMessage( + short ? messages.minutes : messages.minutes_full, + { number: Math.floor(delta / MINUTE) } + ); + } else if (delta < DAY) { + relativeTime = intl.formatMessage( + short ? messages.hours : messages.hours_full, + { number: Math.floor(delta / HOUR) } + ); + } else { + relativeTime = intl.formatMessage( + short ? messages.days : messages.days_full, + { number: Math.floor(delta / DAY) } + ); + } + } else if (date.getFullYear() === year) { + relativeTime = intl.formatDate(date, shortDateFormatOptions); + } else { + relativeTime = intl.formatDate(date, { + ...shortDateFormatOptions, + year: 'numeric', + }); + } + + return relativeTime; +}; + +const timeRemainingString = ( + intl: IntlShape, + date: Date, + now: number, + timeGiven = true +) => { + const delta = date.getTime() - now; + + let relativeTime; + + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { + relativeTime = intl.formatMessage(messages.moments_remaining); + } else if (delta < MINUTE) { + relativeTime = intl.formatMessage(messages.seconds_remaining, { + number: Math.floor(delta / SECOND), + }); + } else if (delta < HOUR) { + relativeTime = intl.formatMessage(messages.minutes_remaining, { + number: Math.floor(delta / MINUTE), + }); + } else if (delta < DAY) { + relativeTime = intl.formatMessage(messages.hours_remaining, { + number: Math.floor(delta / HOUR), + }); + } else { + relativeTime = intl.formatMessage(messages.days_remaining, { + number: Math.floor(delta / DAY), + }); + } + + return relativeTime; +}; + +interface Props { + intl: IntlShape; + timestamp: string; + year: number; + futureDate?: boolean; + short?: boolean; +} +interface States { + now: number; +} +class RelativeTimestamp extends Component { + state = { + now: Date.now(), + }; + + static defaultProps = { + year: new Date().getFullYear(), + short: true, + }; + + _timer: number | undefined; + + shouldComponentUpdate(nextProps: Props, nextState: States) { + // As of right now the locale doesn't change without a new page load, + // but we might as well check in case that ever changes. + return ( + this.props.timestamp !== nextProps.timestamp || + this.props.intl.locale !== nextProps.intl.locale || + this.state.now !== nextState.now + ); + } + + UNSAFE_componentWillReceiveProps(nextProps: Props) { + if (this.props.timestamp !== nextProps.timestamp) { + this.setState({ now: Date.now() }); + } + } + + componentDidMount() { + this._scheduleNextUpdate(this.props, this.state); + } + + UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) { + this._scheduleNextUpdate(nextProps, nextState); + } + + componentWillUnmount() { + window.clearTimeout(this._timer); + } + + _scheduleNextUpdate(props: Props, state: States) { + window.clearTimeout(this._timer); + + const { timestamp } = props; + const delta = new Date(timestamp).getTime() - state.now; + const unitDelay = getUnitDelay(selectUnits(delta)); + const unitRemainder = Math.abs(delta % unitDelay); + const updateInterval = 1000 * 10; + const delay = + delta < 0 + ? Math.max(updateInterval, unitDelay - unitRemainder) + : Math.max(updateInterval, unitRemainder); + + this._timer = window.setTimeout(() => { + this.setState({ now: Date.now() }); + }, delay); + } + + render() { + const { timestamp, intl, year, futureDate, short } = this.props; + + const timeGiven = timestamp.includes('T'); + const date = new Date(timestamp); + const relativeTime = futureDate + ? timeRemainingString(intl, date, this.state.now, timeGiven) + : timeAgoString(intl, date, this.state.now, year, timeGiven, short); + + return ( + + ); + } +} + +const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp); + +export { RelativeTimestampWithIntl as RelativeTimestamp }; diff --git a/app/javascript/flavours/glitch/components/scrollable_list.jsx b/app/javascript/flavours/glitch/components/scrollable_list.jsx index fc7dc989d..8d18c2081 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.jsx +++ b/app/javascript/flavours/glitch/components/scrollable_list.jsx @@ -1,19 +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 { 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']), @@ -90,15 +99,19 @@ class ScrollableList extends PureComponent { lastScrollWasSynthetic = false; scrollToTopOnMouseIdle = false; + _getScrollingElement = () => { + if (this.props.bindToDocument) { + return (document.scrollingElement || document.body); + } else { + return this.node; + } + }; + setScrollTop = newScrollTop => { if (this.getScrollTop() !== newScrollTop) { this.lastScrollWasSynthetic = true; - if (this.props.bindToDocument) { - document.scrollingElement.scrollTop = newScrollTop; - } else { - this.node.scrollTop = newScrollTop; - } + this._getScrollingElement().scrollTop = newScrollTop; } }; @@ -106,6 +119,7 @@ class ScrollableList extends PureComponent { if (this.mouseIdleTimer === null) { return; } + clearTimeout(this.mouseIdleTimer); this.mouseIdleTimer = null; }; @@ -113,13 +127,13 @@ class ScrollableList extends PureComponent { handleMouseMove = throttle(() => { // As long as the mouse keeps moving, clear and restart the idle timer. this.clearMouseIdleTimer(); - this.mouseIdleTimer = - setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); + this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); if (!this.mouseMovedRecently && this.getScrollTop() === 0) { // Only set if we just started moving and are scrolled to the top. this.scrollToTopOnMouseIdle = true; } + // Save setting this flag for last, so we can do the comparison above. this.mouseMovedRecently = true; }, MOUSE_IDLE_DELAY / 2); @@ -134,6 +148,7 @@ class ScrollableList extends PureComponent { if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) { this.setScrollTop(0); } + this.mouseMovedRecently = false; this.scrollToTopOnMouseIdle = false; }; @@ -141,6 +156,7 @@ class ScrollableList extends PureComponent { componentDidMount () { this.attachScrollListener(); this.attachIntersectionObserver(); + attachFullscreenListener(this.onFullScreenChange); // Handle initial scroll position @@ -156,15 +172,15 @@ class ScrollableList extends PureComponent { }; getScrollTop = () => { - return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop; + return this._getScrollingElement().scrollTop; }; getScrollHeight = () => { - return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight; + return this._getScrollingElement().scrollHeight; }; getClientHeight = () => { - return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight; + return this._getScrollingElement().clientHeight; }; updateScrollBottom = (snapshot) => { @@ -173,13 +189,9 @@ class ScrollableList extends PureComponent { this.setScrollTop(newScrollTop); }; - cacheMediaWidth = (width) => { - if (width && this.state.cachedMediaWidth != width) this.setState({ cachedMediaWidth: width }); - }; - - getSnapshotBeforeUpdate (prevProps, prevState) { - const someItemInserted = React.Children.count(prevProps.children) > 0 && - React.Children.count(prevProps.children) < React.Children.count(this.props.children) && + getSnapshotBeforeUpdate (prevProps) { + 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); @@ -198,10 +210,17 @@ class ScrollableList extends PureComponent { } } + cacheMediaWidth = (width) => { + if (width && this.state.cachedMediaWidth !== width) { + this.setState({ cachedMediaWidth: width }); + } + }; + componentWillUnmount () { this.clearMouseIdleTimer(); this.detachScrollListener(); this.detachIntersectionObserver(); + detachFullscreenListener(this.onFullScreenChange); } @@ -210,10 +229,13 @@ class ScrollableList extends PureComponent { }; attachIntersectionObserver () { - this.intersectionObserverWrapper.connect({ + let nodeOptions = { root: this.node, rootMargin: '300% 0px', - }); + }; + + this.intersectionObserverWrapper + .connect(this.props.bindToDocument ? {} : nodeOptions); } detachIntersectionObserver () { @@ -223,20 +245,20 @@ class ScrollableList extends PureComponent { attachScrollListener () { if (this.props.bindToDocument) { document.addEventListener('scroll', this.handleScroll); - document.addEventListener('wheel', this.handleWheel); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel); + 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); } } @@ -277,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; @@ -303,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 ba84064a8..4809df1dd 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/server_hero_image.tsx b/app/javascript/flavours/glitch/components/server_hero_image.tsx new file mode 100644 index 000000000..be05059e4 --- /dev/null +++ b/app/javascript/flavours/glitch/components/server_hero_image.tsx @@ -0,0 +1,36 @@ +import { useCallback, useState } from 'react'; +import * as React from 'react'; + +import classNames from 'classnames'; + +import { Blurhash } from './blurhash'; + +interface Props { + src: string; + srcSet?: string; + blurhash?: string; + className?: string; +} + +export const ServerHeroImage: React.FC = ({ + src, + srcSet, + blurhash, + className, +}) => { + const [loaded, setLoaded] = useState(false); + + const handleLoad = useCallback(() => { + setLoaded(true); + }, [setLoaded]); + + return ( +
    + {blurhash && } + +
    + ); +}; diff --git a/app/javascript/flavours/glitch/components/setting_text.jsx b/app/javascript/flavours/glitch/components/setting_text.jsx index 3a21a0601..79d4bf8ea 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 535c17727..0ddd26e78 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 /** @@ -24,7 +26,6 @@ import { FormattedMessage, FormattedNumber } from 'react-intl'; /** * Component that renders short big number to a shorter version - * * @param {ShortNumberProps} param0 Props for the component * @returns {JSX.Element} Rendered number */ @@ -32,17 +33,14 @@ function ShortNumber({ value, renderer, children }) { const shortNumber = toShortNumber(value); const [, division] = shortNumber; - // eslint-disable-next-line eqeqeq if (children != null && renderer != null) { console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.'); } - // eslint-disable-next-line eqeqeq const customRenderer = children != null ? children : renderer; const displayNumber = ; - // eslint-disable-next-line eqeqeq return customRenderer != null ? customRenderer(displayNumber, pluralReady(value, division)) : displayNumber; @@ -61,7 +59,6 @@ ShortNumber.propTypes = { /** * Renders short number into corresponding localizable react fragment - * * @param {ShortNumberCounterProps} param0 Props for the component * @returns {JSX.Element} FormattedMessage ready to be embedded in code */ @@ -114,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 6a17ffb26..000000000 --- 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 000000000..30ff0d852 --- /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/spoilers.jsx b/app/javascript/flavours/glitch/components/spoilers.jsx deleted file mode 100644 index 75e4ec3a1..000000000 --- a/app/javascript/flavours/glitch/components/spoilers.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -export default -class Spoilers extends React.PureComponent { - - static propTypes = { - spoilerText: PropTypes.string, - children: PropTypes.node, - }; - - state = { - hidden: true, - }; - - handleSpoilerClick = () => { - this.setState({ hidden: !this.state.hidden }); - }; - - render () { - const { spoilerText, children } = this.props; - const { hidden } = this.state; - - const toggleText = hidden ? - () : - (); - - return ([ -

    - {spoilerText} - {' '} - -

    , -
    - {children} -
    , - ]); - } - -} - diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 4321191e9..b48d72d05 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -1,35 +1,44 @@ -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, visibleReactions } 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 StatusPrepend from './status_prepend'; import StatusReactions from './status_reactions'; -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, visibleReactions } from 'flavours/glitch/initial_state'; -import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; -// 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']), ]; @@ -69,6 +78,9 @@ class Status extends ImmutablePureComponent { id: PropTypes.string, status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, + previousId: PropTypes.string, + nextInReplyToId: PropTypes.string, + rootId: PropTypes.string, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -135,6 +147,9 @@ class Status extends ImmutablePureComponent { 'expanded', 'unread', 'pictureInPicture', + 'previousId', + 'nextInReplyToId', + 'rootId', ]; updateOnStates = [ @@ -280,7 +295,7 @@ class Status extends ImmutablePureComponent { // Hack to fix timeline jumps on second rendering when auto-collapsing // or on subsequent rendering when a preview card has been fetched - getSnapshotBeforeUpdate (prevProps, prevState) { + getSnapshotBeforeUpdate() { if (!this.props.getScrollPosition) return null; const { muted, hidden, status, settings } = this.props; @@ -295,7 +310,7 @@ class Status extends ImmutablePureComponent { } } - componentDidUpdate (prevProps, prevState, snapshot) { + componentDidUpdate(prevProps, prevState, snapshot) { if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) { this.props.updateScrollBottom(snapshot.height - snapshot.top); } @@ -366,9 +381,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(); } @@ -388,11 +401,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 => { @@ -402,10 +418,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); } } }; @@ -439,16 +456,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 => { @@ -459,7 +472,7 @@ class Status extends ImmutablePureComponent { this.props.onMoveDown(this.props.containerId || this.props.id, e.target.getAttribute('data-featured')); }; - handleHotkeyCollapse = e => { + handleHotkeyCollapse = () => { if (!this.props.settings.getIn(['collapsed', 'enabled'])) return; @@ -503,7 +516,6 @@ class Status extends ImmutablePureComponent { const { handleRef, parseClick, - setExpansion, setCollapsed, } = this; const { router } = this.context; @@ -522,9 +534,12 @@ class Status extends ImmutablePureComponent { unread, featured, pictureInPicture, + previousId, + nextInReplyToId, + rootId, ...other } = this.props; - const { isCollapsed, forceFilter } = this.state; + const { isCollapsed } = this.state; let background = null; let attachments = null; @@ -565,10 +580,12 @@ class Status extends ImmutablePureComponent { openMedia: this.handleHotkeyOpenMedia, }; + let prepend, rebloggedByText; + if (hidden) { return ( -
    +
    {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.get('content')}
    @@ -576,7 +593,11 @@ class Status extends ImmutablePureComponent { ); } + const connectUp = previousId && previousId === status.get('in_reply_to_id'); + const connectToRoot = rootId && rootId === status.get('in_reply_to_id'); + const connectReply = nextInReplyToId && nextInReplyToId === status.get('id'); const matchedFilters = status.get('matched_filters'); + if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) { const minHandlers = this.props.muted ? {} : { moveUp: this.handleHotkeyMoveUp, @@ -585,7 +606,7 @@ class Status extends ImmutablePureComponent { return ( -
    +
    : {matchedFilters.join(', ')}. {' '} + ); + } + +} + +class Emoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.string.isRequired, + hovered: PropTypes.bool.isRequired, + url: PropTypes.string, + staticUrl: PropTypes.string, + }; + + render() { + const { emoji, hovered, url, staticUrl } = this.props; + + if (unicodeMapping[emoji]) { + const { filename, shortCode } = unicodeMapping[this.props.emoji]; + const title = shortCode ? `:${shortCode}:` : ''; + + return ( + {emoji} + ); + } else { + const filename = (autoPlayGif || hovered) ? url : staticUrl; + const shortCode = `:${emoji}:`; + + return ( + {shortCode} + ); + } + } + +} diff --git a/app/javascript/flavours/glitch/components/status_visibility_icon.jsx b/app/javascript/flavours/glitch/components/status_visibility_icon.jsx index fcedfbfd6..ad84af4de 100644 --- a/app/javascript/flavours/glitch/components/status_visibility_icon.jsx +++ b/app/javascript/flavours/glitch/components/status_visibility_icon.jsx @@ -1,14 +1,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 { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ 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' }, }); diff --git a/app/javascript/flavours/glitch/components/timeline_hint.jsx b/app/javascript/flavours/glitch/components/timeline_hint.jsx deleted file mode 100644 index fb55a62cc..000000000 --- a/app/javascript/flavours/glitch/components/timeline_hint.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -const TimelineHint = ({ resource, url }) => ( -
    - -
    - -
    -); - -TimelineHint.propTypes = { - resource: PropTypes.node.isRequired, - url: PropTypes.string.isRequired, -}; - -export default TimelineHint; diff --git a/app/javascript/flavours/glitch/components/timeline_hint.tsx b/app/javascript/flavours/glitch/components/timeline_hint.tsx new file mode 100644 index 000000000..df7787bf6 --- /dev/null +++ b/app/javascript/flavours/glitch/components/timeline_hint.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +interface Props { + resource: JSX.Element; + url: string; +} + +export const TimelineHint: React.FC = ({ resource, url }) => ( +
    + + + +
    + + + +
    +); diff --git a/app/javascript/flavours/glitch/containers/account_container.jsx b/app/javascript/flavours/glitch/containers/account_container.jsx index 5b57d730f..f20454585 100644 --- a/app/javascript/flavours/glitch/containers/account_container.jsx +++ b/app/javascript/flavours/glitch/containers/account_container.jsx @@ -1,8 +1,7 @@ -import React from 'react'; -import { connect } from 'react-redux'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { makeGetAccount } from 'flavours/glitch/selectors'; -import Account from 'flavours/glitch/components/account'; + +import { connect } from 'react-redux'; + import { followAccount, unfollowAccount, @@ -13,7 +12,9 @@ import { } from 'flavours/glitch/actions/accounts'; import { openModal } from 'flavours/glitch/actions/modal'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import Account from 'flavours/glitch/components/account'; import { unfollowModal } from 'flavours/glitch/initial_state'; +import { makeGetAccount } from 'flavours/glitch/selectors'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, @@ -34,10 +35,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')}
    }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); diff --git a/app/javascript/flavours/glitch/containers/admin_component.jsx b/app/javascript/flavours/glitch/containers/admin_component.jsx index 64dabac8b..06c846f4d 100644 --- a/app/javascript/flavours/glitch/containers/admin_component.jsx +++ b/app/javascript/flavours/glitch/containers/admin_component.jsx @@ -1,23 +1,19 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; +import { PureComponent } from 'react'; -const { localeData, messages } = getLocale(); -addLocaleData(localeData); +import { IntlProvider } from 'flavours/glitch/locales'; -export default class AdminComponent extends React.PureComponent { +export default class AdminComponent extends PureComponent { static propTypes = { - locale: PropTypes.string.isRequired, children: PropTypes.node.isRequired, }; render () { - const { locale, children } = this.props; + const { children } = this.props; return ( - + {children} ); diff --git a/app/javascript/flavours/glitch/containers/compose_container.jsx b/app/javascript/flavours/glitch/containers/compose_container.jsx index 1e49b89a0..f92bf9797 100644 --- a/app/javascript/flavours/glitch/containers/compose_container.jsx +++ b/app/javascript/flavours/glitch/containers/compose_container.jsx @@ -1,18 +1,13 @@ -import React from 'react'; +import { PureComponent } from 'react'; + import { Provider } from 'react-redux'; -import PropTypes from 'prop-types'; -import configureStore from 'flavours/glitch/store/configureStore'; + +import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; import { hydrateStore } from 'flavours/glitch/actions/store'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; import Compose from 'flavours/glitch/features/standalone/compose'; import initialState from 'flavours/glitch/initial_state'; -import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -const store = configureStore(); +import { IntlProvider } from 'flavours/glitch/locales'; +import { store } from 'flavours/glitch/store'; if (initialState) { store.dispatch(hydrateStore(initialState)); @@ -20,17 +15,11 @@ if (initialState) { store.dispatch(fetchCustomEmojis()); -export default class TimelineContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; +export default class ComposeContainer extends PureComponent { render () { - const { locale } = this.props; - return ( - + diff --git a/app/javascript/flavours/glitch/containers/domain_container.jsx b/app/javascript/flavours/glitch/containers/domain_container.jsx index e92e102ab..c719a5775 100644 --- a/app/javascript/flavours/glitch/containers/domain_container.jsx +++ b/app/javascript/flavours/glitch/containers/domain_container.jsx @@ -1,27 +1,30 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { blockDomain, unblockDomain } from '../actions/domain_blocks'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import Domain from '../components/domain'; + +import { connect } from 'react-redux'; + +import { blockDomain, unblockDomain } from '../actions/domain_blocks'; import { openModal } from '../actions/modal'; +import { Domain } from '../components/domain'; const messages = defineMessages({ blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, }); const makeMapStateToProps = () => { - const mapStateToProps = (state, { }) => ({ - }); + const mapStateToProps = () => ({}); return mapStateToProps; }; const mapDispatchToProps = (dispatch, { intl }) => ({ onBlockDomain (domain) { - dispatch(openModal('CONFIRM', { - message: {domain}
    }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + }, })); }, diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index 43ce8ca63..da67602b5 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -1,7 +1,9 @@ +import { connect } from 'react-redux'; + import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; import { openModal, closeModal } from 'flavours/glitch/actions/modal'; -import { connect } from 'react-redux'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; + import { isUserTouching } from '../is_mobile'; const mapStateToProps = state => ({ @@ -11,15 +13,21 @@ const mapStateToProps = state => ({ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ onOpen(id, onItemClick, keyboard) { - dispatch(isUserTouching() ? openModal('ACTIONS', { - status, - actions: items, - onClick: onItemClick, + dispatch(isUserTouching() ? openModal({ + modalType: 'ACTIONS', + modalProps: { + status, + actions: items, + onClick: onItemClick, + }, }) : openDropdownMenu(id, keyboard, scrollKey)); }, onClose(id) { - dispatch(closeModal('ACTIONS')); + dispatch(closeModal({ + modalType: 'ACTIONS', + ignoreFocus: false, + })); dispatch(closeDropdownMenu(id)); }, }); diff --git a/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js index f2741f2d4..11aedd527 100644 --- a/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js +++ b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; -import IntersectionObserverArticle from 'flavours/glitch/components/intersection_observer_article'; + import { setHeight } from 'flavours/glitch/actions/height_cache'; +import IntersectionObserverArticle from 'flavours/glitch/components/intersection_observer_article'; const makeMapStateToProps = (state, props) => ({ cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]), diff --git a/app/javascript/flavours/glitch/containers/mastodon.jsx b/app/javascript/flavours/glitch/containers/mastodon.jsx index dd7623a81..ae2eb0b60 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.jsx +++ b/app/javascript/flavours/glitch/containers/mastodon.jsx @@ -1,26 +1,25 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PureComponent } from 'react'; + import { Helmet } from 'react-helmet'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter, Route } from 'react-router-dom'; + +import { Provider as ReduxProvider } from 'react-redux'; + import { ScrollContext } from 'react-router-scroll-4'; -import configureStore from 'flavours/glitch/store/configureStore'; -import UI from 'flavours/glitch/features/ui'; + import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; -import { hydrateStore } from 'flavours/glitch/actions/store'; import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings'; +import { hydrateStore } from 'flavours/glitch/actions/store'; import { connectUserStream } from 'flavours/glitch/actions/streaming'; import ErrorBoundary from 'flavours/glitch/components/error_boundary'; +import UI from 'flavours/glitch/features/ui'; import initialState, { title as siteTitle } from 'flavours/glitch/initial_state'; -import { getLocale } from 'locales'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); +import { IntlProvider } from 'flavours/glitch/locales'; +import { store } from 'flavours/glitch/store'; const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; -export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); @@ -39,11 +38,7 @@ const createIdentityContext = state => ({ permissions: state.role ? state.role.permissions : 0, }); -export default class Mastodon extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; +export default class Mastodon extends PureComponent { static childContextTypes = { identity: PropTypes.shape({ @@ -80,10 +75,8 @@ export default class Mastodon extends React.PureComponent { } render () { - const { locale } = this.props; - return ( - + diff --git a/app/javascript/flavours/glitch/containers/media_container.jsx b/app/javascript/flavours/glitch/containers/media_container.jsx index 37b5484e6..52aac5ebe 100644 --- a/app/javascript/flavours/glitch/containers/media_container.jsx +++ b/app/javascript/flavours/glitch/containers/media_container.jsx @@ -1,47 +1,45 @@ -import React, { PureComponent, Fragment } from 'react'; -import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; +import { PureComponent } from 'react'; +import { createPortal } from 'react-dom'; + import { fromJS } from 'immutable'; -import { getLocale } from 'mastodon/locales'; -import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; -import MediaGallery from 'flavours/glitch/components/media_gallery'; -import Poll from 'flavours/glitch/components/poll'; + import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; +import MediaGallery from 'flavours/glitch/components/media_gallery'; import ModalRoot from 'flavours/glitch/components/modal_root'; +import Poll from 'flavours/glitch/components/poll'; +import Audio from 'flavours/glitch/features/audio'; +import Card from 'flavours/glitch/features/status/components/card'; import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; import Video from 'flavours/glitch/features/video'; -import Card from 'flavours/glitch/features/status/components/card'; -import Audio from 'flavours/glitch/features/audio'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); +import { IntlProvider } from 'flavours/glitch/locales'; +import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; export default class MediaContainer extends PureComponent { static propTypes = { - locale: PropTypes.string.isRequired, components: PropTypes.object.isRequired, }; state = { media: null, index: null, + lang: null, time: null, backgroundColor: null, options: null, }; - handleOpenMedia = (media, index) => { + handleOpenMedia = (media, index, lang) => { document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media, index }); + this.setState({ media, index, lang }); }; - handleOpenVideo = (options) => { + handleOpenVideo = (lang, options) => { const { components } = this.props; const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const mediaList = fromJS(media); @@ -49,12 +47,12 @@ export default class MediaContainer extends PureComponent { document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media: mediaList, options }); + this.setState({ media: mediaList, lang, options }); }; handleCloseMedia = () => { document.body.classList.remove('with-modals--active'); - document.documentElement.style.marginRight = 0; + document.documentElement.style.marginRight = '0'; this.setState({ media: null, @@ -70,11 +68,18 @@ export default class MediaContainer extends PureComponent { }; render () { - const { locale, components } = this.props; + const { components } = this.props; + + let handleOpenVideo; + + // Don't offer to expand the video in a lightbox if we're in a frame + if (window.self === window.top) { + handleOpenVideo = this.handleOpenVideo; + } return ( - - + + <> {[].map.call(components, (component, i) => { const componentName = component.getAttribute('data-component'); const Component = MEDIA_COMPONENTS[componentName]; @@ -88,13 +93,13 @@ export default class MediaContainer extends PureComponent { ...(componentName === 'Video' ? { componentIndex: i, - onOpenVideo: this.handleOpenVideo, + onOpenVideo: handleOpenVideo, } : { onOpenMedia: this.handleOpenMedia, }), }); - return ReactDOM.createPortal( + return createPortal( , component, ); @@ -105,6 +110,7 @@ export default class MediaContainer extends PureComponent { )} - + ); } diff --git a/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js index 2570cf4a5..144d77f13 100644 --- a/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js +++ b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js @@ -1,15 +1,16 @@ // Package imports. -import { connect } from 'react-redux'; import { defineMessages, injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; + // Our imports. -import NotificationPurgeButtons from 'flavours/glitch/components/notification_purge_buttons'; +import { openModal } from 'flavours/glitch/actions/modal'; import { deleteMarkedNotifications, enterNotificationClearingMode, markAllNotifications, } from 'flavours/glitch/actions/notifications'; -import { openModal } from 'flavours/glitch/actions/modal'; +import NotificationPurgeButtons from 'flavours/glitch/components/notification_purge_buttons'; const messages = defineMessages({ clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' }, @@ -22,10 +23,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onDeleteMarked() { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(deleteMarkedNotifications()), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.clearMessage), + confirm: intl.formatMessage(messages.clearConfirm), + onConfirm: () => dispatch(deleteMarkedNotifications()), + }, })); }, diff --git a/app/javascript/flavours/glitch/containers/poll_container.js b/app/javascript/flavours/glitch/containers/poll_container.js index 345351cc6..e25dd0614 100644 --- a/app/javascript/flavours/glitch/containers/poll_container.js +++ b/app/javascript/flavours/glitch/containers/poll_container.js @@ -1,8 +1,9 @@ import { connect } from 'react-redux'; + import { debounce } from 'lodash'; -import Poll from 'flavours/glitch/components/poll'; import { fetchPoll, vote } from 'flavours/glitch/actions/polls'; +import Poll from 'flavours/glitch/components/poll'; const mapDispatchToProps = (dispatch, { pollId }) => ({ refresh: debounce( diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 7faed925c..ddb9ec87a 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -1,12 +1,17 @@ +import { defineMessages, injectIntl } from 'react-intl'; + import { connect } from 'react-redux'; -import Status from 'flavours/glitch/components/status'; -import { List as ImmutableList } from 'immutable'; -import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; + +import { initBlockModal } from 'flavours/glitch/actions/blocks'; +import { initBoostModal } from 'flavours/glitch/actions/boosts'; import { replyCompose, mentionCompose, directCompose, } from 'flavours/glitch/actions/compose'; +import { + initAddFilter, +} from 'flavours/glitch/actions/filters'; import { reblog, favourite, @@ -19,6 +24,11 @@ import { addReaction, removeReaction, } from 'flavours/glitch/actions/interactions'; +import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; +import { initReport } from 'flavours/glitch/actions/reports'; import { muteStatus, unmuteStatus, @@ -29,29 +39,17 @@ import { translateStatus, undoStatusTranslation, } from 'flavours/glitch/actions/statuses'; -import { - initAddFilter, -} from 'flavours/glitch/actions/filters'; -import { initMuteModal } from 'flavours/glitch/actions/mutes'; -import { initBlockModal } from 'flavours/glitch/actions/blocks'; -import { initReport } from 'flavours/glitch/actions/reports'; -import { initBoostModal } from 'flavours/glitch/actions/boosts'; -import { openModal } from 'flavours/glitch/actions/modal'; -import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; -import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Status from 'flavours/glitch/components/status'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state'; -import { filterEditLink } from 'flavours/glitch/utils/backend_links'; +import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; + import { showAlertForError } from '../actions/alerts'; -import AccountContainer from 'flavours/glitch/containers/account_container'; -import Spoilers from '../components/spoilers'; -import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, - redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, + redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' }, @@ -85,6 +83,7 @@ const makeMapStateToProps = () => { return { containerId: props.containerId || props.id, // Should match reblogStatus's id for reblogs status: status, + nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null, account: account || props.account, settings: state.get('local_settings'), prepend: prepend || props.prepend, @@ -102,11 +101,14 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ let state = getState(); if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), - onConfirm: () => dispatch(replyCompose(status, router)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), + onConfirm: () => dispatch(replyCompose(status, router)), + }, })); } else { dispatch(replyCompose(status, router)); @@ -154,7 +156,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ if (e.shiftKey || !favouriteModal) { this.onModalFavourite(status); } else { - dispatch(openModal('FAVOURITE', { status, onFavourite: this.onModalFavourite })); + dispatch(openModal({ + modalType: 'FAVOURITE', + modalProps: { + status, + onFavourite: this.onModalFavourite, + }, + })); } } }, @@ -176,9 +184,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onEmbed (status) { - dispatch(openModal('EMBED', { - url: status.get('url'), - onError: error => dispatch(showAlertForError(error)), + dispatch(openModal({ + modalType: 'EMBED', + modalProps: { + url: status.get('url'), + onError: error => dispatch(showAlertForError(error)), + }, })); }, @@ -186,10 +197,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ if (!deleteModal) { dispatch(deleteStatus(status.get('id'), history, withRedraft)); } else { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), - confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), + }, })); } }, @@ -198,10 +212,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ dispatch((_, getState) => { let state = getState(); if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.editMessage), - confirm: intl.formatMessage(messages.editConfirm), - onConfirm: () => dispatch(editStatus(status.get('id'), history)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.editMessage), + confirm: intl.formatMessage(messages.editConfirm), + onConfirm: () => dispatch(editStatus(status.get('id'), history)), + }, })); } else { dispatch(editStatus(status.get('id'), history)); @@ -211,7 +228,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ onTranslate (status) { if (status.get('translation')) { - dispatch(undoStatusTranslation(status.get('id'))); + dispatch(undoStatusTranslation(status.get('id'), status.get('poll'))); } else { dispatch(translateStatus(status.get('id'))); } @@ -225,12 +242,18 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ dispatch(mentionCompose(account, router)); }, - onOpenMedia (statusId, media, index) { - dispatch(openModal('MEDIA', { statusId, media, index })); + onOpenMedia (statusId, media, index, lang) { + dispatch(openModal({ + modalType: 'MEDIA', + modalProps: { statusId, media, index, lang }, + })); }, - onOpenVideo (statusId, media, options) { - dispatch(openModal('VIDEO', { statusId, media, options })); + onOpenVideo (statusId, media, lang, options) { + dispatch(openModal({ + modalType: 'VIDEO', + modalProps: { statusId, media, lang, options }, + })); }, onBlock (status) { @@ -275,10 +298,13 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onInteractionModal (type, status) { - dispatch(openModal('INTERACTION', { - type, - accountId: status.getIn(['account', 'id']), - url: status.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type, + accountId: status.getIn(['account', 'id']), + url: status.get('url'), + }, })); }, diff --git a/app/javascript/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx index f366f734d..fe07a870b 100644 --- a/app/javascript/flavours/glitch/features/about/index.jsx +++ b/app/javascript/flavours/glitch/features/about/index.jsx @@ -1,17 +1,21 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import Column from 'flavours/glitch/components/column'; -import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; -import { Helmet } from 'react-helmet'; -import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server'; -import Account from 'flavours/glitch/containers/account_container'; -import Skeleton from 'flavours/glitch/components/skeleton'; -import Icon from 'flavours/glitch/components/icon'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import classNames from 'classnames'; -import Image from 'flavours/glitch/components/image'; +import { Helmet } from 'react-helmet'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; + +import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server'; +import Column from 'flavours/glitch/components/column'; +import { Icon } from 'flavours/glitch/components/icon'; +import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; +import Account from 'flavours/glitch/containers/account_container'; +import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, @@ -41,7 +45,7 @@ const mapStateToProps = state => ({ domainBlocks: state.getIn(['server', 'domainBlocks']), }); -class Section extends React.PureComponent { +class Section extends PureComponent { static propTypes = { title: PropTypes.string, @@ -67,7 +71,7 @@ class Section extends React.PureComponent { return (
    -
    +
    {title}
    @@ -80,7 +84,7 @@ class Section extends React.PureComponent { } -class About extends React.PureComponent { +class About extends PureComponent { static propTypes = { server: ImmutablePropTypes.map, @@ -114,7 +118,7 @@ class About extends React.PureComponent {
    - `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> + `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />

    {isLoading ? : server.get('domain')}

    Mastodon }} />

    @@ -157,7 +161,7 @@ class About extends React.PureComponent {
    - {!isLoading && (server.get('rules').isEmpty() ? ( + {!isLoading && (server.get('rules', []).isEmpty() ? (

    ) : (
      diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.jsx b/app/javascript/flavours/glitch/features/account/components/account_note.jsx index 5adca87d0..041f8de98 100644 --- a/app/javascript/flavours/glitch/features/account/components/account_note.jsx +++ b/app/javascript/flavours/glitch/features/account/components/account_note.jsx @@ -1,11 +1,14 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Icon from 'flavours/glitch/components/icon'; + import Textarea from 'react-textarea-autosize'; +import { Icon } from 'flavours/glitch/components/icon'; + const messages = defineMessages({ placeholder: { id: 'account_note.glitch_placeholder', defaultMessage: 'No comment provided' }, }); @@ -53,11 +56,11 @@ class Header extends ImmutablePureComponent { if (isEditing) { action_buttons = (
      -
      -
      @@ -65,7 +68,7 @@ class Header extends ImmutablePureComponent { } else { action_buttons = (
      -
      diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.jsx b/app/javascript/flavours/glitch/features/account/components/action_bar.jsx index e32bc0141..46a766925 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.jsx @@ -1,18 +1,17 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; -import { NavLink } from 'react-router-dom'; -import { injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; -import { me, isStaff } from 'flavours/glitch/initial_state'; -import { profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; -import Icon from 'flavours/glitch/components/icon'; +import { PureComponent } from 'react'; -class ActionBar extends React.PureComponent { +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { NavLink } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import { Icon } from 'flavours/glitch/components/icon'; + +class ActionBar extends PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, }; isStatusesPageActive = (match, location) => { @@ -23,7 +22,7 @@ class ActionBar extends React.PureComponent { }; render () { - const { account, intl } = this.props; + const { account } = this.props; if (account.get('suspended')) { return ( @@ -83,4 +82,4 @@ class ActionBar extends React.PureComponent { } -export default injectIntl(ActionBar); +export default ActionBar; diff --git a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx index 42e0a8d2f..87e88f2fa 100644 --- a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx +++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx @@ -1,8 +1,10 @@ -import React from 'react'; import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import Hashtag from 'flavours/glitch/components/hashtag'; const messages = defineMessages({ diff --git a/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx b/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx index 73c1737a6..7368ce975 100644 --- a/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx +++ b/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx @@ -1,8 +1,9 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Icon from 'flavours/glitch/components/icon'; + +import { Icon } from 'flavours/glitch/components/icon'; export default class FollowRequestNote extends ImmutablePureComponent { diff --git a/app/javascript/flavours/glitch/features/account/components/header.jsx b/app/javascript/flavours/glitch/features/account/components/header.jsx index 6f918abcf..0c440dc8a 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account/components/header.jsx @@ -1,21 +1,24 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; -import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; + import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import IconButton from 'flavours/glitch/components/icon_button'; -import Avatar from 'flavours/glitch/components/avatar'; +import { Helmet } from 'react-helmet'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Avatar } from 'flavours/glitch/components/avatar'; import Button from 'flavours/glitch/components/button'; -import { NavLink } from 'react-router-dom'; +import { Icon } from 'flavours/glitch/components/icon'; +import { IconButton } from 'flavours/glitch/components/icon_button'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; +import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; +import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; + import AccountNoteContainer from '../containers/account_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container'; -import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; -import { Helmet } from 'react-helmet'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -27,7 +30,7 @@ const messages = defineMessages({ linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' }, account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' }, mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, - direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' }, + direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, @@ -147,7 +150,6 @@ class Header extends ImmutablePureComponent { const { account } = this.props; navigator.share({ - text: `${titleFromAccount(account)}\n${account.get('note_plain')}`, url: account.get('url'), }).catch((e) => { if (e.name !== 'AbortError') console.error(e); @@ -270,16 +272,16 @@ class Header extends ImmutablePureComponent { if (account.getIn(['relationship', 'muting'])) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); } else { - menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true }); } if (account.getIn(['relationship', 'blocking'])) { menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true }); } - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); } if (signedIn && isRemote) { @@ -288,7 +290,7 @@ class Header extends ImmutablePureComponent { if (account.getIn(['relationship', 'domain_blocking'])) { menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain }); } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain }); + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true }); } } @@ -312,7 +314,7 @@ class Header extends ImmutablePureComponent { let badge; if (account.get('bot')) { - badge = (
      ); + badge = (
      ); } else if (account.get('group')) { badge = (
      ); } else { @@ -346,10 +348,10 @@ class Header extends ImmutablePureComponent { {!suspended && (
      {!hidden && ( - + <> {actionBtn} {bellBtn} - + )} @@ -396,6 +398,7 @@ class Header extends ImmutablePureComponent { {titleFromAccount(account)} +
      ); diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx b/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx index 62a459fff..2dc4216bd 100644 --- a/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx +++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx @@ -1,13 +1,15 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ColumnHeader from '../../../components/column_header'; +import { PureComponent } from 'react'; + import { injectIntl, defineMessages } from 'react-intl'; +import ColumnHeader from '../../../components/column_header'; + const messages = defineMessages({ profile: { id: 'column_header.profile', defaultMessage: 'Profile' }, }); -class ProfileColumnHeader extends React.PureComponent { +class ProfileColumnHeader extends PureComponent { static propTypes = { onClick: PropTypes.func, diff --git a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js index f1d007ecb..51d229c84 100644 --- a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js +++ b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js @@ -1,5 +1,7 @@ import { connect } from 'react-redux'; + import { changeAccountNoteComment, submitAccountNote, initEditAccountNote, cancelAccountNote } from 'flavours/glitch/actions/account_notes'; + import AccountNote from '../components/account_note'; const mapStateToProps = (state, { account }) => { diff --git a/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js index 6f0b06941..bafdcba80 100644 --- a/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js +++ b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js @@ -1,7 +1,9 @@ -import { connect } from 'react-redux'; -import FeaturedTags from '../components/featured_tags'; -import { makeGetAccount } from 'flavours/glitch/selectors'; import { List as ImmutableList } from 'immutable'; +import { connect } from 'react-redux'; + +import { makeGetAccount } from 'flavours/glitch/selectors'; + +import FeaturedTags from '../components/featured_tags'; const mapStateToProps = () => { const getAccount = makeGetAccount(); diff --git a/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js b/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js index c6a3afb7e..3b2ffbadf 100644 --- a/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js +++ b/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js @@ -1,7 +1,9 @@ import { connect } from 'react-redux'; -import FollowRequestNote from '../components/follow_request_note'; + import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts'; +import FollowRequestNote from '../components/follow_request_note'; + const mapDispatchToProps = (dispatch, { account }) => ({ onAuthorize () { dispatch(authorizeFollowRequest(account.get('id'))); diff --git a/app/javascript/flavours/glitch/features/account/navigation.jsx b/app/javascript/flavours/glitch/features/account/navigation.jsx index b8b8e54de..4be00c49f 100644 --- a/app/javascript/flavours/glitch/features/account/navigation.jsx +++ b/app/javascript/flavours/glitch/features/account/navigation.jsx @@ -1,6 +1,8 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { connect } from 'react-redux'; + import FeaturedTags from 'flavours/glitch/features/account/containers/featured_tags_container'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; @@ -19,7 +21,7 @@ const mapStateToProps = (state, { match: { params: { acct } } }) => { }; }; -class AccountNavigation extends React.PureComponent { +class AccountNavigation extends PureComponent { static propTypes = { match: PropTypes.shape({ diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx index 5fd84996b..4453b557d 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx @@ -1,12 +1,16 @@ -import Blurhash from 'flavours/glitch/components/blurhash'; -import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; import PropTypes from 'prop-types'; -import React from 'react'; + +import classNames from 'classnames'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Blurhash } from 'flavours/glitch/components/blurhash'; +import { Icon } from 'flavours/glitch/components/icon'; +import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; + + + export default class MediaItem extends ImmutablePureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.jsx b/app/javascript/flavours/glitch/features/account_gallery/index.jsx index 6914bcba7..2da679fc2 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.jsx +++ b/app/javascript/flavours/glitch/features/account_gallery/index.jsx @@ -1,21 +1,25 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; -import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; + +import { FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { getAccountGallery } from 'flavours/glitch/selectors'; -import MediaItem from './components/media_item'; -import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; -import ScrollContainer from 'flavours/glitch/containers/scroll_container'; -import LoadMore from 'flavours/glitch/components/load_more'; -import MissingIndicator from 'flavours/glitch/components/missing_indicator'; +import { connect } from 'react-redux'; + +import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; import { openModal } from 'flavours/glitch/actions/modal'; +import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; +import { LoadMore } from 'flavours/glitch/components/load_more'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import ScrollContainer from 'flavours/glitch/containers/scroll_container'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; +import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; +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 { getAccountGallery } from 'flavours/glitch/selectors'; + +import MediaItem from './components/media_item'; const mapStateToProps = (state, { params: { acct, id } }) => { const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); @@ -71,8 +75,8 @@ class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, - multiColumn: PropTypes.bool, suspended: PropTypes.bool, + multiColumn: PropTypes.bool, }; state = { @@ -141,16 +145,26 @@ class AccountGallery extends ImmutablePureComponent { handleOpenMedia = attachment => { const { dispatch } = this.props; const statusId = attachment.getIn(['status', 'id']); + const lang = attachment.getIn(['status', 'language']); if (attachment.get('type') === 'video') { - dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal({ + modalType: 'VIDEO', + modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } }, + })); } else if (attachment.get('type') === 'audio') { - dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal({ + modalType: 'AUDIO', + modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } }, + })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); - dispatch(openModal('MEDIA', { media, index, statusId })); + dispatch(openModal({ + modalType: 'MEDIA', + modalProps: { media, index, statusId, lang }, + })); } }; @@ -166,9 +180,7 @@ class AccountGallery extends ImmutablePureComponent { if (!isAccount) { return ( - - - + ); } diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx index eec065b43..717114d5c 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx @@ -1,11 +1,16 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import InnerHeader from 'flavours/glitch/features/account/components/header'; -import ActionBar from 'flavours/glitch/features/account/components/action_bar'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { FormattedMessage } from 'react-intl'; + import { NavLink } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import ActionBar from 'flavours/glitch/features/account/components/action_bar'; +import InnerHeader from 'flavours/glitch/features/account/components/header'; + +import MemorialNote from './memorial_note'; import MovedNote from './moved_note'; export default class Header extends ImmutablePureComponent { @@ -116,6 +121,7 @@ export default class Header extends ImmutablePureComponent { return (
      + {(!hidden && account.get('memorial')) && } {(!hidden && account.get('moved')) && } - +
      )} diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx index c622b7607..5ea37a5d3 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx @@ -1,8 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { revealAccount } from 'flavours/glitch/actions/accounts'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; + +import { connect } from 'react-redux'; + +import { revealAccount } from 'flavours/glitch/actions/accounts'; import Button from 'flavours/glitch/components/button'; import { domain } from 'flavours/glitch/initial_state'; @@ -14,7 +17,7 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({ }); -class LimitedAccountHint extends React.PureComponent { +class LimitedAccountHint extends PureComponent { static propTypes = { accountId: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/memorial_note.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/memorial_note.jsx new file mode 100644 index 000000000..a04808f1c --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/components/memorial_note.jsx @@ -0,0 +1,11 @@ +import { FormattedMessage } from 'react-intl'; + +const MemorialNote = () => ( +
      +
      + +
      +
      +); + +export default MemorialNote; diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx index 40bdc4034..2e10ea94a 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx @@ -1,11 +1,14 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; + import { FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Icon } from 'flavours/glitch/components/icon'; + import AvatarOverlay from '../../../components/avatar_overlay'; -import DisplayName from '../../../components/display_name'; -import Icon from 'flavours/glitch/components/icon'; +import { DisplayName } from '../../../components/display_name'; export default class MovedNote extends ImmutablePureComponent { @@ -21,9 +24,7 @@ export default class MovedNote extends ImmutablePureComponent { handleAccountClick = e => { if (e.button === 0) { e.preventDefault(); - let state = { ...this.context.router.history.location.state }; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/@${this.props.to.get('acct')}`, state); + this.context.router.history.push(`/@${this.props.to.get('acct')}`); } e.stopPropagation(); @@ -37,7 +38,7 @@ export default class MovedNote extends ImmutablePureComponent {
      - }} /> + }} />
      diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx index 3ec47cf2f..270865df4 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx @@ -1,7 +1,8 @@ -import React from 'react'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import { connect } from 'react-redux'; -import { makeGetAccount, getAccountHidden } from 'flavours/glitch/selectors'; -import Header from '../components/header'; + +import { initEditAccountNote } from 'flavours/glitch/actions/account_notes'; import { followAccount, unfollowAccount, @@ -10,23 +11,24 @@ import { pinAccount, unpinAccount, } from 'flavours/glitch/actions/accounts'; +import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { mentionCompose, directCompose, } from 'flavours/glitch/actions/compose'; -import { initMuteModal } from 'flavours/glitch/actions/mutes'; -import { initBlockModal } from 'flavours/glitch/actions/blocks'; -import { initReport } from 'flavours/glitch/actions/reports'; -import { openModal } from 'flavours/glitch/actions/modal'; import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks'; -import { initEditAccountNote } from 'flavours/glitch/actions/account_notes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { initReport } from 'flavours/glitch/actions/reports'; import { unfollowModal } from 'flavours/glitch/initial_state'; +import { makeGetAccount, getAccountHidden } from 'flavours/glitch/selectors'; + +import Header from '../components/header'; const messages = defineMessages({ cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, - blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, }); const makeMapStateToProps = () => { @@ -46,20 +48,26 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); } } else if (account.getIn(['relationship', 'requested'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')}
      }} />, + confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); @@ -70,10 +78,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onInteractionModal (account) { - dispatch(openModal('INTERACTION', { - type: 'follow', - accountId: account.get('id'), - url: account.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'follow', + accountId: account.get('id'), + url: account.get('url'), + }, })); }, @@ -93,10 +104,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(directCompose(account, router)); }, - onDirect (account, router) { - dispatch(directCompose(account, router)); - }, - onReblogToggle (account) { if (account.getIn(['relationship', 'showing_reblogs'])) { dispatch(followAccount(account.get('id'), { reblogs: false })); @@ -138,10 +145,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onBlockDomain (domain) { - dispatch(openModal('CONFIRM', { - message: {domain} }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + }, })); }, @@ -150,21 +160,30 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onAddToList (account) { - dispatch(openModal('LIST_ADDER', { - accountId: account.get('id'), + dispatch(openModal({ + modalType: 'LIST_ADDER', + modalProps: { + accountId: account.get('id'), + }, })); }, onChangeLanguages (account) { - dispatch(openModal('SUBSCRIBED_LANGUAGES', { - accountId: account.get('id'), + dispatch(openModal({ + modalType: 'SUBSCRIBED_LANGUAGES', + modalProps: { + accountId: account.get('id'), + }, })); }, onOpenAvatar (account) { - dispatch(openModal('IMAGE', { - src: account.get('avatar'), - alt: account.get('acct'), + dispatch(openModal({ + modalType: 'IMAGE', + modalProps: { + src: account.get('avatar'), + alt: account.get('acct'), + }, })); }, diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.jsx b/app/javascript/flavours/glitch/features/account_timeline/index.jsx index 38361b1ca..03c989e96 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/index.jsx @@ -1,24 +1,34 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; -import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines'; -import StatusList from '../../components/status_list'; -import LoadingIndicator from '../../components/loading_indicator'; -import Column from '../ui/components/column'; -import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; -import HeaderContainer from './containers/header_container'; -import ColumnBackButton from 'flavours/glitch/components/column_back_button'; -import { List as ImmutableList } from 'immutable'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { FormattedMessage } from 'react-intl'; -import MissingIndicator from 'flavours/glitch/components/missing_indicator'; -import TimelineHint from 'flavours/glitch/components/timeline_hint'; -import LimitedAccountHint from './components/limited_account_hint'; -import { getAccountHidden } from 'flavours/glitch/selectors'; + +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 { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; +import { TimelineHint } from 'flavours/glitch/components/timeline_hint'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; +import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import { getAccountHidden } from 'flavours/glitch/selectors'; + import { fetchFeaturedTags } from '../../actions/featured_tags'; +import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; +import { LoadingIndicator } from '../../components/loading_indicator'; +import StatusList from '../../components/status_list'; +import Column from '../ui/components/column'; + +import LimitedAccountHint from './components/limited_account_hint'; +import HeaderContainer from './containers/header_container'; + + + + + + + const emptyList = ImmutableList(); @@ -123,7 +133,7 @@ class AccountTimeline extends ImmutablePureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { const { dispatch } = this.props; if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { @@ -160,10 +170,7 @@ class AccountTimeline extends ImmutablePureComponent { ); } else if (!isLoading && !isAccount) { return ( - - - - + ); } diff --git a/app/javascript/flavours/glitch/features/audio/index.jsx b/app/javascript/flavours/glitch/features/audio/index.jsx index fd7229cc5..1c4130666 100644 --- a/app/javascript/flavours/glitch/features/audio/index.jsx +++ b/app/javascript/flavours/glitch/features/audio/index.jsx @@ -1,15 +1,23 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video'; -import Icon from 'flavours/glitch/components/icon'; + import classNames from 'classnames'; -import { throttle, debounce } from 'lodash'; -import Visualizer from './visualizer'; -import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; -import Blurhash from 'flavours/glitch/components/blurhash'; + import { is } from 'immutable'; +import { throttle, debounce } from 'lodash'; + +import { Blurhash } from 'flavours/glitch/components/blurhash'; +import { Icon } from 'flavours/glitch/components/icon'; +import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video'; +import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; + +import Visualizer from './visualizer'; + + + const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, pause: { id: 'video.pause', defaultMessage: 'Pause' }, @@ -22,7 +30,7 @@ const messages = defineMessages({ const TICK_SIZE = 10; const PADDING = 180; -class Audio extends React.PureComponent { +class Audio extends PureComponent { static propTypes = { src: PropTypes.string.isRequired, @@ -94,7 +102,7 @@ class Audio extends React.PureComponent { const width = this.player.offsetWidth; const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9)); - if (width && width != this.state.containerWidth) { + if (width && width !== this.state.containerWidth) { if (this.props.cacheWidth) { this.props.cacheWidth(width); } @@ -142,7 +150,7 @@ class Audio extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { this.setState({ revealed: nextProps.visible }); } @@ -390,7 +398,7 @@ class Audio extends React.PureComponent { } _getRadius () { - return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2); + return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient()); } _getScaleCoefficient () { @@ -402,7 +410,7 @@ class Audio extends React.PureComponent { } _getCY() { - return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient())); + return Math.floor((this.state.height || this.props.height) / 2); } _getAccentColor () { @@ -476,7 +484,7 @@ class Audio extends React.PureComponent { } return ( -
      +
      }
      @@ -532,7 +547,7 @@ class Audio extends React.PureComponent { @@ -549,7 +564,7 @@ class Audio extends React.PureComponent {
      diff --git a/app/javascript/flavours/glitch/features/blocks/index.jsx b/app/javascript/flavours/glitch/features/blocks/index.jsx index 461dac2ec..aa5479b20 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.jsx +++ b/app/javascript/flavours/glitch/features/blocks/index.jsx @@ -1,16 +1,20 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { debounce } from 'lodash'; import PropTypes from 'prop-types'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import ScrollableList from '../../components/scrollable_list'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; -import AccountContainer from 'flavours/glitch/containers/account_container'; -import { fetchBlocks, expandBlocks } from 'flavours/glitch/actions/blocks'; + import { defineMessages, injectIntl, 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 { fetchBlocks, expandBlocks } from 'flavours/glitch/actions/blocks'; +import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import AccountContainer from 'flavours/glitch/containers/account_container'; +import Column from 'flavours/glitch/features/ui/components/column'; + +import ScrollableList from '../../components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.blocks', defaultMessage: 'Blocked users' }, @@ -34,7 +38,7 @@ class Blocks extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBlocks()); } diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx index 90d8fd0ef..c674c8254 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx @@ -1,23 +1,28 @@ -import { debounce } from 'lodash'; import PropTypes from 'prop-types'; -import React from 'react'; -import { Helmet } from 'react-helmet'; + 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 { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; 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.bookmarks', defaultMessage: 'Bookmarks' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'bookmarks', 'items']), + statusIds: getStatusList(state, 'bookmarks'), isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), }); @@ -34,7 +39,7 @@ class Bookmarks extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBookmarkedStatuses()); } diff --git a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx index 1f17ea9cf..b556da391 100644 --- a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx +++ b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx @@ -1,9 +1,10 @@ -import React from 'react'; -import { connect } from 'react-redux'; import { FormattedMessage } from 'react-intl'; + import ImmutablePureComponent from 'react-immutable-pure-component'; -import { domain } from 'flavours/glitch/initial_state'; +import { connect } from 'react-redux'; + import { fetchServer } from 'flavours/glitch/actions/server'; +import { domain } from 'flavours/glitch/initial_state'; const mapStateToProps = state => ({ message: state.getIn(['server', 'server', 'registrations', 'message']), diff --git a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx index 0ea874e95..1e93125d5 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/community_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'; @@ -10,7 +13,7 @@ const messages = defineMessages({ settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -class ColumnSettings extends React.PureComponent { +class ColumnSettings extends PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js index eac1c4bba..dbfc4594e 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/community_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 { changeSetting } from 'flavours/glitch/actions/settings'; +import ColumnSettings from '../components/column_settings'; + const mapStateToProps = (state, { columnId }) => { const uuid = columnId; const columns = state.getIn(['settings', 'columns']); diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.jsx b/app/javascript/flavours/glitch/features/community_timeline/index.jsx index 8f3e10fe9..ca11adb46 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/community_timeline/index.jsx @@ -1,17 +1,22 @@ -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 { connectCommunityStream } from 'flavours/glitch/actions/streaming'; +import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectCommunityStream } from 'flavours/glitch/actions/streaming'; -import { Helmet } from 'react-helmet'; -import { domain } from 'flavours/glitch/initial_state'; import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { domain } from 'flavours/glitch/initial_state'; + +import ColumnSettingsContainer from './containers/column_settings_container'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, @@ -32,7 +37,7 @@ const mapStateToProps = (state, { columnId }) => { }; }; -class CommunityTimeline extends React.PureComponent { +class CommunityTimeline extends PureComponent { static defaultProps = { onlyMedia: false, @@ -137,11 +142,8 @@ class CommunityTimeline extends React.PureComponent { - - - - } trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} timelineId={`community${onlyMedia ? ':media' : ''}`} diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx index af1f02efc..f155979ef 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -1,10 +1,14 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; + const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, @@ -14,14 +18,14 @@ const messages = defineMessages({ lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, 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' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, }); -class ActionBar extends React.PureComponent { +class ActionBar extends PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, @@ -58,7 +62,7 @@ class ActionBar extends React.PureComponent { return (
      - +
      ); diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx index fb9bb5035..5f00da52c 100644 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx @@ -1,9 +1,9 @@ -import React from 'react'; -import Avatar from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; 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'; + export default class AutosuggestAccount extends ImmutablePureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx index 0ecfc9141..42452b30f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx @@ -1,8 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { length } from 'stringz'; -export default class CharacterCounter extends React.PureComponent { +export default class CharacterCounter extends PureComponent { static propTypes = { text: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index 973a17a1a..53e1bf79a 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -1,23 +1,28 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ReplyIndicatorContainer from '../containers/reply_indicator_container'; -import AutosuggestTextarea from '../../../components/autosuggest_textarea'; -import AutosuggestInput from '../../../components/autosuggest_input'; + import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { length } from 'stringz'; + +import { maxChars } from 'flavours/glitch/initial_state'; +import { isMobile } from 'flavours/glitch/is_mobile'; + +import AutosuggestInput from '../../../components/autosuggest_input'; +import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; +import OptionsContainer from '../containers/options_container'; import PollFormContainer from '../containers/poll_form_container'; +import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import UploadFormContainer from '../containers/upload_form_container'; import WarningContainer from '../containers/warning_container'; -import { isMobile } from 'flavours/glitch/is_mobile'; -import ImmutablePureComponent from 'react-immutable-pure-component'; import { countableText } from '../util/counter'; -import OptionsContainer from '../containers/options_container'; + +import CharacterCounter from './character_counter'; import Publisher from './publisher'; import TextareaIcons from './textarea_icons'; -import { maxChars } from 'flavours/glitch/initial_state'; -import CharacterCounter from './character_counter'; -import { length } from 'stringz'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, @@ -76,7 +81,6 @@ class ComposeForm extends ImmutablePureComponent { preselectOnReply: PropTypes.bool, onChangeSpoilerness: PropTypes.func, onChangeVisibility: PropTypes.func, - onPaste: PropTypes.func, onMediaDescriptionConfirm: PropTypes.func, }; @@ -164,11 +168,11 @@ class ComposeForm extends ImmutablePureComponent { }; // Selects a suggestion from the autofill. - onSuggestionSelected = (tokenStart, token, value) => { + handleSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['text']); }; - onSpoilerSuggestionSelected = (tokenStart, token, value) => { + handleSpoilerSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); }; @@ -177,7 +181,7 @@ class ComposeForm extends ImmutablePureComponent { this.handleSubmit(); } - if (e.keyCode == 13 && e.altKey) { + if (e.keyCode === 13 && e.altKey) { this.handleSecondarySubmit(); } }; @@ -281,9 +285,7 @@ class ComposeForm extends ImmutablePureComponent { const { handleEmojiPick, handleSecondarySubmit, - handleSelect, handleSubmit, - handleRefTextarea, } = this; const { advancedOptions, @@ -291,7 +293,6 @@ class ComposeForm extends ImmutablePureComponent { isSubmitting, layout, onChangeSpoilerness, - onChangeVisibility, onClearSuggestions, onFetchSuggestions, onPaste, @@ -322,10 +323,10 @@ class ComposeForm extends ImmutablePureComponent { onKeyDown={this.handleKeyDown} disabled={!spoiler} ref={this.handleRefSpoilerText} - suggestions={this.props.suggestions} + suggestions={suggestions} onSuggestionsFetchRequested={onFetchSuggestions} onSuggestionsClearRequested={onClearSuggestions} - onSuggestionSelected={this.onSpoilerSuggestionSelected} + onSuggestionSelected={this.handleSpoilerSuggestionSelected} searchTokens={[':']} id='glitch.composer.spoiler.input' className='spoiler-input__input' @@ -342,11 +343,11 @@ class ComposeForm extends ImmutablePureComponent { value={this.props.text} onChange={this.handleChange} onKeyDown={this.handleKeyDown} - suggestions={this.props.suggestions} + suggestions={suggestions} onFocus={this.handleFocus} onSuggestionsFetchRequested={onFetchSuggestions} onSuggestionsClearRequested={onClearSuggestions} - onSuggestionSelected={this.onSuggestionSelected} + onSuggestionSelected={this.handleSuggestionSelected} onPaste={onPaste} autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} lang={this.props.lang} diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx index fe4ab36f5..a50413ebe 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx @@ -1,18 +1,18 @@ // Package imports. -import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; +import { PureComponent } from 'react'; + +import classNames from 'classnames'; + import Overlay from 'react-overlays/Overlay'; // Components. -import IconButton from 'flavours/glitch/components/icon_button'; +import { IconButton } from 'flavours/glitch/components/icon_button'; + import DropdownMenu from './dropdown_menu'; -// Utils. -import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; - // The component. -export default class ComposerOptionsDropdown extends React.PureComponent { +export default class ComposerOptionsDropdown extends PureComponent { static propTypes = { isUserTouching: PropTypes.func, @@ -50,7 +50,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { const { open } = this.state; if (this.props.isUserTouching && this.props.isUserTouching()) { - if (this.state.open) { + if (open) { this.props.onModalClose(); } else { const modal = this.handleMakeModal(); @@ -59,10 +59,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent { } } } else { - if (this.state.open && this.activeElement) { + if (open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' }); + this.setState({ open: !open, openedViaKeyboard: type !== 'click' }); } }; diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx index 1ea0df536..a78e6914c 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx @@ -1,18 +1,18 @@ // Package imports. import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { PureComponent } from 'react'; + import classNames from 'classnames'; -// Components. -import Icon from 'flavours/glitch/components/icon'; +import { supportsPassiveEvents } from 'detect-passive-events'; -// Utils. -import { withPassive } from 'flavours/glitch/utils/dom_helpers'; -import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; +// Components. +import { Icon } from 'flavours/glitch/components/icon'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; // The component. -export default class ComposerOptionsDropdownContent extends React.PureComponent { +export default class ComposerOptionsDropdownContent extends PureComponent { static propTypes = { items: PropTypes.arrayOf(PropTypes.shape({ @@ -43,6 +43,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent handleDocumentClick = (e) => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } }; @@ -53,8 +54,8 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // On mounting, we add our listeners. componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, withPassive); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem) { this.focusedItem.focus({ preventScroll: true }); } else { @@ -64,8 +65,8 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // On unmounting, we remove our listeners. componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, withPassive); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } handleClick = (e) => { @@ -78,7 +79,8 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent items, } = this.props; - const { name } = this.props.items[i]; + const { name } = items[i]; + e.preventDefault(); // Prevents change in focus on click if (closeOnChange) { onClose(); @@ -131,7 +133,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent if (element) { element.focus(); - this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name); + this.handleChange(items[Number(element.getAttribute('data-index'))].name); e.preventDefault(); e.stopPropagation(); } @@ -152,14 +154,14 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent if (!contents) { contents = ( - + <> {icon && }
      {text} {meta}
      -
      + ); } @@ -169,7 +171,8 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent onClick={this.handleClick} onKeyDown={this.handleKeyDown} role='option' - tabIndex='0' + aria-selected={active} + tabIndex={0} key={name} data-index={i} ref={active ? this.setFocusRef : null} @@ -183,8 +186,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent render () { const { items, - onChange, - onClose, style, } = this.props; diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index 1e44008ea..01cd3d5dd 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -1,15 +1,21 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; -import Overlay from 'react-overlays/Overlay'; + import classNames from 'classnames'; + import ImmutablePropTypes from 'react-immutable-proptypes'; + import { supportsPassiveEvents } from 'detect-passive-events'; -import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; +import Overlay from 'react-overlays/Overlay'; + import { useSystemEmojiFont } from 'flavours/glitch/initial_state'; import { assetHost } from 'flavours/glitch/utils/config'; +import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; +import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; + const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, @@ -28,7 +34,7 @@ const messages = defineMessages({ let EmojiPicker, Emoji; // load asynchronously -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`; @@ -48,7 +54,7 @@ const notFoundFn = () => (
      ); -class ModifierPickerMenu extends React.PureComponent { +class ModifierPickerMenu extends PureComponent { static propTypes = { active: PropTypes.bool, @@ -60,7 +66,7 @@ class ModifierPickerMenu extends React.PureComponent { this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.active) { this.attachListeners(); } else { @@ -79,12 +85,12 @@ class ModifierPickerMenu extends React.PureComponent { }; attachListeners () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); } removeListeners () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -109,7 +115,7 @@ class ModifierPickerMenu extends React.PureComponent { } -class ModifierPicker extends React.PureComponent { +class ModifierPicker extends PureComponent { static propTypes = { active: PropTypes.bool, @@ -145,7 +151,7 @@ class ModifierPicker extends React.PureComponent { } -class EmojiPickerMenuImpl extends React.PureComponent { +class EmojiPickerMenuImpl extends PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, @@ -177,7 +183,7 @@ class EmojiPickerMenuImpl extends React.PureComponent { }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need @@ -192,7 +198,7 @@ class EmojiPickerMenuImpl extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -308,7 +314,7 @@ class EmojiPickerMenuImpl extends React.PureComponent { const EmojiPickerMenu = injectIntl(EmojiPickerMenuImpl); -class EmojiPickerDropdown extends React.PureComponent { +class EmojiPickerDropdown extends PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, @@ -386,7 +392,7 @@ class EmojiPickerDropdown extends React.PureComponent { {button || 🙂}
      diff --git a/app/javascript/flavours/glitch/features/compose/components/header.jsx b/app/javascript/flavours/glitch/features/compose/components/header.jsx index 764fcec5e..ac6d4dce8 100644 --- a/app/javascript/flavours/glitch/features/compose/components/header.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/header.jsx @@ -1,19 +1,16 @@ -// Package imports. import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; + import { injectIntl, defineMessages } from 'react-intl'; + import { Link } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; +import { Icon } from 'flavours/glitch/components/icon'; import { signOutLink } from 'flavours/glitch/utils/backend_links'; +import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; -// Messages. const messages = defineMessages({ community: { defaultMessage: 'Local timeline', @@ -77,7 +74,7 @@ class Header extends ImmutablePureComponent { // The result. return ( -