diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 3913a6b0f..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,128 +0,0 @@ -version: 2.1 - -orbs: - ruby: circleci/ruby@2.0.0 - node: circleci/node@5.0.3 - -executors: - default: - parameters: - ruby-version: - type: string - docker: - - image: cimg/ruby:<< parameters.ruby-version >> - environment: - BUNDLE_JOBS: 3 - BUNDLE_RETRY: 3 - CONTINUOUS_INTEGRATION: true - DB_HOST: localhost - DB_USER: root - DISABLE_SIMPLECOV: true - RAILS_ENV: test - - image: cimg/postgres:14.5 - environment: - POSTGRES_USER: root - POSTGRES_HOST_AUTH_METHOD: trust - - image: cimg/redis:7.0 - -commands: - install-system-dependencies: - steps: - - run: - name: Install system dependencies - command: | - sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev - install-ruby-dependencies: - parameters: - ruby-version: - type: string - steps: - - run: - command: | - bundle config clean 'true' - bundle config frozen 'true' - bundle config without 'development production' - name: Set bundler settings - - ruby/install-deps: - bundler-version: '2.3.26' - key: ruby<< parameters.ruby-version >>-gems-v2 - wait-db: - steps: - - run: - command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m - name: Wait for PostgreSQL and Redis - -jobs: - build: - docker: - - image: cimg/ruby:3.2-node - environment: - RAILS_ENV: test - steps: - - checkout - - install-system-dependencies - - install-ruby-dependencies: - ruby-version: '3.2' - - node/install-packages: - cache-version: v1 - pkg-manager: yarn - - run: - command: | - export NODE_OPTIONS=--openssl-legacy-provider - ./bin/rails assets:precompile - name: Precompile assets - - persist_to_workspace: - paths: - - public/assets - - public/packs-test - root: . - - test: - parameters: - ruby-version: - type: string - executor: - name: default - ruby-version: << parameters.ruby-version >> - environment: - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - parallelism: 4 - steps: - - checkout - - install-system-dependencies - - run: - command: sudo apt-get install -y ffmpeg imagemagick libmagickcore-dev libmagickwand-dev libjpeg-dev libpng-dev libtiff-dev libwebp-dev libpam-dev - name: Install additional system dependencies - - run: - command: bundle config with 'pam_authentication' - name: Enable PAM authentication - - install-ruby-dependencies: - ruby-version: << parameters.ruby-version >> - - attach_workspace: - at: . - - wait-db - - run: - command: ./bin/rails db:create db:schema:load db:seed - name: Load database schema - - ruby/rspec-test - -workflows: - version: 2 - build-and-test: - jobs: - - build - - test: - matrix: - parameters: - ruby-version: - - '2.7' - - '3.0' - - '3.1' - - '3.2' - name: test-ruby<< matrix.ruby-version >> - requires: - - build diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 00469df00..000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: '2' -checks: - argument-count: - enabled: false - complex-logic: - enabled: false - file-lines: - enabled: false - method-complexity: - enabled: false - method-count: - enabled: false - method-lines: - enabled: false - nested-control-flow: - enabled: false - return-statements: - enabled: false - similar-code: - enabled: false - identical-code: - enabled: false -plugins: - brakeman: - enabled: true - bundler-audit: - enabled: false - eslint: - enabled: false - rubocop: - enabled: false - sass-lint: - enabled: false -exclude_patterns: - - spec/ - - vendor/asset/ - - - app/javascript/mastodon/locales/**/*.json - - config/locales/**/*.yml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 425b86a6b..04ac9560c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,16 +1,14 @@ -# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster -ARG VARIANT=3.1-bullseye -FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT} +# For details, see https://github.com/devcontainers/images/tree/main/src/ruby +FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye # Install Rails # RUN gem install rails webdrivers # Default value to allow debug server to serve content over GitHub Codespace's port forwarding service # The value is a comma-separated list of allowed domains -ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev" +ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev" -# [Choice] Node.js version: lts/*, 18, 16, 14 -ARG NODE_VERSION="lts/*" +ARG NODE_VERSION="16" RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" # [Optional] Uncomment this section to install additional OS packages. @@ -22,3 +20,5 @@ RUN gem install foreman # [Optional] Uncomment this line to install global node packages. RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1 + +COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6ac6993ee..17208a84e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,30 +1,13 @@ +// For more details, see https://aka.ms/devcontainer.json. { "name": "Mastodon", "dockerComposeFile": "docker-compose.yml", "service": "app", - "workspaceFolder": "/mastodon", - - // Configure tool-specific properties. - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - // Set *default* container specific settings.json values on container create. - "settings": {}, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "EditorConfig.EditorConfig", - "dbaeumer.vscode-eslint", - "rebornix.Ruby", - "webben.browserslist" - ] - } - }, + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Features to add to the dev container. More info: https://containers.dev/features. "features": { - "ghcr.io/devcontainers/features/sshd:1": { - "version": "latest" - } + "ghcr.io/devcontainers/features/sshd:1": {} }, // Use 'forwardPorts' to make a list of ports inside the container available locally. @@ -32,8 +15,18 @@ "forwardPorts": [3000, 4000], // Use 'postCreateCommand' to run commands after the container is created. + "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "postCreateCommand": ".devcontainer/post-create.sh", + "waitFor": "postCreateCommand", - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": {}, + // Add the IDs of extensions you want installed when the container is created. + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] + } + } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 95f401379..4eebb74d1 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,19 +5,12 @@ services: build: context: . dockerfile: Dockerfile - args: - # Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6 - # Append -bullseye or -buster to pin to an OS version. - # Use -bullseye variants on local arm64/Apple Silicon. - VARIANT: '3.0-bullseye' - # Optional Node.js version to install - NODE_VERSION: '16' volumes: - - ..:/mastodon:cached + - ../..:/workspaces:cached environment: RAILS_ENV: development NODE_ENV: development - + BIND: 0.0.0.0 REDIS_HOST: redis REDIS_PORT: '6379' DB_HOST: db @@ -30,10 +23,12 @@ services: LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 # Overrides default command so things don't shut down after the process ends. command: sleep infinity + ports: + - '127.0.0.1:3000:3000' + - '127.0.0.1:4000:4000' networks: - external_network - internal_network - user: vscode db: image: postgres:14-alpine @@ -49,7 +44,7 @@ services: - internal_network redis: - image: redis:6-alpine + image: redis:7-alpine restart: unless-stopped volumes: - redis-data:/data @@ -74,15 +69,19 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.2.9 + image: libretranslate/libretranslate:v1.3.10 restart: unless-stopped + volumes: + - lt-data:/home/libretranslate/.local networks: + - external_network - internal_network volumes: postgres-data: redis-data: es-data: + lt-data: networks: external_network: diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 02f488f12..a075cc7b3 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -3,17 +3,22 @@ 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 +# [re]create, migrate, and seed the development database +RAILS_ENV=development ./bin/rails db:setup + # Precompile assets for development RAILS_ENV=development ./bin/rails assets:precompile diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt new file mode 100644 index 000000000..488cf9285 --- /dev/null +++ b/.devcontainer/welcome-message.txt @@ -0,0 +1,8 @@ +👋 Welcome to "Mastodon" in GitHub Codespaces! + +🛠️ Your environment is fully setup with all the required software. + +🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1). + +📝 Edit away, run your app as usual, and we'll automatically make it available for you to access. + diff --git a/.eslintrc.js b/.eslintrc.js index 606a87e41..badcb1dcb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,74 +4,65 @@ 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: { browser: true, node: true, es6: true, - jest: true, }, globals: { ATTACHMENT_HOST: false, }, - parser: '@babel/eslint-parser', + parser: '@typescript-eslint/parser', plugins: [ 'react', 'jsx-a11y', 'import', 'promise', + '@typescript-eslint', + 'formatjs', ], parserOptions: { sourceType: 'module', ecmaFeatures: { - experimentalObjectRestSpread: true, jsx: true, }, ecmaVersion: 2021, + requireConfigFile: false, + babelOptions: { + configFile: false, + presets: ['@babel/react', '@babel/env'], + }, }, settings: { react: { version: 'detect', }, - 'import/extensions': [ - '.js', '.jsx', - ], 'import/ignore': [ 'node_modules', '\\.(css|scss|json)$', ], 'import/resolver': { - node: { - paths: ['app/javascript'], - extensions: ['.js', '.jsx'], - }, + 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,40 +82,25 @@ module.exports = { { property: 'substr', message: 'Use .slice instead of .substr.' }, ], 'no-self-assign': 'off', - 'no-trailing-spaces': 'warn', 'no-unused-expressions': 'error', - 'no-unused-vars': [ + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ 'error', { 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', { 'allow': 'as-needed' }], + '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-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-target-blank': 'off', - 'react/jsx-tag-spacing': 'error', - 'react/jsx-wrap-multilines': 'error', - 'react/no-deprecated': 'off', 'react/no-unknown-property': 'off', 'react/self-closing-comp': 'error', @@ -188,19 +164,31 @@ 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', { devDependencies: [ 'config/webpack/**', + 'app/javascript/mastodon/performance.js', 'app/javascript/mastodon/test_setup.js', 'app/javascript/**/__tests__/**', ], }, ], + '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', 'promise/always-return': 'off', @@ -213,5 +201,163 @@ 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: [ + { + files: [ + '*.config.js', + '.*rc.js', + 'ide-helper.js', + 'config/webpack/**/*', + ], + + env: { + commonjs: true, + }, + + parserOptions: { + sourceType: 'script', + }, + + rules: { + 'import/no-commonjs': 'off', + }, + }, + { + files: [ + '**/*.ts', + '**/*.tsx', + ], + + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended', + 'plugin:prettier/recommended', + ], + + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + + rules: { + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + + '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,prop-types}', + group: 'builtin', + position: 'after', + }, + // I18n + { + pattern: 'react-intl', + group: 'builtin', + position: 'after', + }, + // Common React utilities + { + pattern: '{classnames,react-helmet}', + 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: [], + }, + ], + + '@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' }], + }, + }, + { + files: [ + '**/__tests__/*.js', + '**/__tests__/*.jsx', + ], + + env: { + jest: true, + }, + }, + { + files: [ + 'streaming/**/*', + ], + rules: { + 'import/no-commonjs': 'off', + }, + }, + ], }; diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index c254418ea..da4203e35 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -25,12 +25,15 @@ jobs: - uses: hadolint/hadolint-action@v3.1.0 - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - - uses: docker/login-action@v2 + + - name: Log in to the Github Container registry + uses: docker/login-action@v2 with: registry: ghcr.io - username: ${{ github.repository_owner }} + username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} if: github.event_name != 'pull_request' + - uses: docker/metadata-action@v4 id: meta with: @@ -39,13 +42,22 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} 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 }} push: ${{ 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/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..e282e2ab7 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -30,13 +30,28 @@ 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 build:development + yarn manage:translations en + 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/haml-lint-problem-matcher.json b/.github/workflows/haml-lint-problem-matcher.json new file mode 100644 index 000000000..3523ea295 --- /dev/null +++ b/.github/workflows/haml-lint-problem-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "haml-lint", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+)\\s\\[W]\\s(.*):\\s(.*)$", + "file": 1, + "line": 2, + "code": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml new file mode 100644 index 000000000..2ddbca781 --- /dev/null +++ b/.github/workflows/lint-haml.yml @@ -0,0 +1,46 @@ +name: Haml Linting +on: + push: + branches-ignore: + - 'dependabot/**' + paths: + - '.github/workflows/haml-lint-problem-matcher.json' + - '.github/workflows/lint-haml.yml' + - '.haml-lint*.yml' + - '.rubocop*.yml' + - '.ruby-version' + - '**/*.haml' + - 'Gemfile*' + + pull_request: + paths: + - '.github/workflows/haml-lint-problem-matcher.json' + - '.github/workflows/lint-haml.yml' + - '.haml-lint*.yml' + - '.rubocop*.yml' + - '.ruby-version' + - '**/*.haml' + - 'Gemfile*' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run haml-lint + run: | + echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" + bundle exec haml-lint diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 44929f63d..7700e4851 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -6,22 +6,28 @@ on: 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 +48,7 @@ jobs: run: yarn --frozen-lockfile - name: ESLint - run: yarn test:lint:js + run: yarn test:lint:js --max-warnings 0 + + - name: Typecheck + run: yarn test:typecheck diff --git a/.github/workflows/rebase-needed.yml b/.github/workflows/rebase-needed.yml index 99b224ec6..6a8035210 100644 --- a/.github/workflows/rebase-needed.yml +++ b/.github/workflows/rebase-needed.yml @@ -2,7 +2,13 @@ name: PR Needs Rebase on: push: + branches-ignore: + - 'dependabot/**' + - 'l10n_main' pull_request_target: + branches-ignore: + - 'dependabot/**' + - 'l10n_main' types: [synchronize] permissions: diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 6a1cacb3f..1c4958550 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -9,6 +9,8 @@ on: - '.nvmrc' - '**/*.js' - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' - '**/*.snap' - '.github/workflows/test-js.yml' @@ -19,6 +21,8 @@ on: - '.nvmrc' - '**/*.js' - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' - '**/*.snap' - '.github/workflows/test-js.yml' diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index 8f070697c..212b2cfe7 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -16,16 +16,24 @@ jobs: - id: skip_check uses: fkirc/skip-duplicate-actions@v5 with: - paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-one-step.yml"]' + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-one-step.yml", "lib/tasks/tests.rake"]' test: runs-on: ubuntu-latest 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.5 + image: postgres:${{ matrix.postgres}} env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres @@ -38,7 +46,7 @@ jobs: - 5432:5432 redis: - image: redis:7.0 + image: redis:7-alpine options: >- --health-cmd "redis-cli ping" --health-interval 10s @@ -64,7 +72,9 @@ jobs: - uses: actions/checkout@v3 - name: Install native Ruby dependencies - run: sudo apt-get install -y libicu-dev libidn11-dev + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev - name: Set up bundler cache uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 2fdce8025..310153929 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -16,16 +16,24 @@ jobs: - id: skip_check uses: fkirc/skip-duplicate-actions@v5 with: - paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-two-step.yml"]' + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-two-step.yml", "lib/tasks/tests.rake"]' test: runs-on: ubuntu-latest 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.5 + image: postgres:${{ matrix.postgres}} env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres @@ -37,7 +45,7 @@ jobs: ports: - 5432:5432 redis: - image: redis:7.0 + image: redis:7-alpine options: >- --health-cmd "redis-cli ping" --health-interval 10s @@ -63,7 +71,9 @@ jobs: - uses: actions/checkout@v3 - name: Install native Ruby dependencies - run: sudo apt-get install -y libicu-dev libidn11-dev + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev - name: Set up bundler cache uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml new file mode 100644 index 000000000..f284745ea --- /dev/null +++ b/.github/workflows/test-ruby.yml @@ -0,0 +1,150 @@ +name: Ruby Testing + +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + +env: + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + mode: + - production + - test + env: + RAILS_ENV: ${{ matrix.mode }} + BUNDLE_WITH: ${{ matrix.mode }} + OTP_SECRET: precompile_placeholder + SECRET_KEY_BASE: precompile_placeholder + + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - run: yarn --frozen-lockfile --production + - name: Precompile assets + # Previously had set this, but it's not supported + # export NODE_OPTIONS=--openssl-legacy-provider + run: |- + ./bin/rails assets:precompile + + - uses: actions/upload-artifact@v3 + if: matrix.mode == 'test' + with: + path: |- + ./public/assets + ./public/packs-test + name: ${{ github.sha }} + retention-days: 0 + + test: + runs-on: ubuntu-latest + + needs: + - build + + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + ALLOW_NOPAM: true + PAM_ENABLED: true + PAM_DEFAULT_SERVICE: pam_test + PAM_CONTROLLED_SERVICE: pam_test_controlled + BUNDLE_WITH: 'pam_authentication test' + CI_JOBS: ${{ matrix.ci_job }}/4 + + strategy: + fail-fast: false + matrix: + ruby-version: + - '3.0' + - '3.1' + - '.ruby-version' + ci_job: + - 1 + - 2 + - 3 + - 4 + steps: + - uses: actions/checkout@v3 + + - uses: actions/download-artifact@v3 + with: + path: './public' + name: ${{ github.sha }} + + - name: Update package index + run: sudo apt-get update + + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Install additional system dependencies + run: sudo apt-get install -y ffmpeg imagemagick libpam-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version}} + bundler-cache: true + + - name: Load database schema + run: './bin/rails db:create db:schema:load db:seed' + + - run: bundle exec rake rspec_chunked diff --git a/.haml-lint.yml b/.haml-lint.yml index 7853d81d7..12ca46342 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -1,108 +1,9 @@ -# Whether to ignore frontmatter at the beginning of HAML documents for -# frameworks such as Jekyll/Middleman -skip_frontmatter: false +inherits_from: .haml-lint_todo.yml exclude: - 'vendor/**/*' - - 'spec/**/*' - - 'lib/templates/**/*' - - 'app/views/kaminari/**/*' + - lib/templates/haml/scaffold/_form.html.haml linters: AltText: - enabled: false - - ClassAttributeWithStaticValue: - enabled: true - - ClassesBeforeIds: - enabled: true - - ConsecutiveComments: - enabled: true - - ConsecutiveSilentScripts: - enabled: true - max_consecutive: 2 - - EmptyObjectReference: - enabled: true - - EmptyScript: - enabled: true - - FinalNewline: - enabled: true - present: true - - HtmlAttributes: - enabled: true - - ImplicitDiv: - enabled: true - - LeadingCommentSpace: - enabled: true - - LineLength: - enabled: false - max: 80 - - MultilinePipe: - enabled: true - - MultilineScript: - enabled: true - - ObjectReferenceAttributes: - enabled: true - - RuboCop: - enabled: true - # These cops are incredibly noisy when it comes to HAML templates, so we - # ignore them. - ignored_cops: - - Lint/BlockAlignment - - Lint/EndAlignment - - Lint/Void - - Metrics/BlockLength - - Metrics/LineLength - - Style/AlignParameters - - Style/BlockNesting - - Style/ElseAlignment - - Style/EndOfLine - - Style/FileName - - Style/FinalNewline - - Style/FrozenStringLiteralComment - - Style/IfUnlessModifier - - Style/IndentationWidth - - Style/Next - - Style/TrailingBlankLines - - Style/TrailingWhitespace - - Style/WhileUntilModifier - - RubyComments: - enabled: true - - SpaceBeforeScript: - enabled: true - - SpaceInsideHashAttributes: - enabled: true - style: space - - Indentation: - enabled: true - character: space # or tab - - TagName: - enabled: true - - TrailingWhitespace: - enabled: true - - UnnecessaryInterpolation: - enabled: true - - UnnecessaryStringOutput: enabled: true diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml new file mode 100644 index 000000000..c60168390 --- /dev/null +++ b/.haml-lint_todo.yml @@ -0,0 +1,106 @@ +# This configuration was generated by +# `haml-lint --auto-gen-config` +# on 2023-03-15 00:55:01 -0400 using Haml-Lint version 0.45.0. +# The point is for the user to remove these configuration records +# one by one as the lints are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of Haml-Lint, may require this file to be generated again. + +linters: + # Offense count: 63 + RuboCop: + exclude: + - 'app/views/accounts/_og.html.haml' + - 'app/views/admin/account_warnings/_account_warning.html.haml' + - 'app/views/admin/accounts/index.html.haml' + - 'app/views/admin/accounts/show.html.haml' + - 'app/views/admin/announcements/edit.html.haml' + - 'app/views/admin/announcements/new.html.haml' + - 'app/views/admin/disputes/appeals/_appeal.html.haml' + - 'app/views/admin/domain_blocks/edit.html.haml' + - 'app/views/admin/domain_blocks/new.html.haml' + - 'app/views/admin/ip_blocks/new.html.haml' + - 'app/views/admin/reports/actions/preview.html.haml' + - 'app/views/admin/reports/index.html.haml' + - 'app/views/admin/reports/show.html.haml' + - 'app/views/admin/roles/_form.html.haml' + - 'app/views/admin/settings/about/show.html.haml' + - 'app/views/admin/settings/appearance/show.html.haml' + - 'app/views/admin/settings/registrations/show.html.haml' + - 'app/views/admin/statuses/show.html.haml' + - 'app/views/auth/registrations/new.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + - 'app/views/filters/_filter_fields.html.haml' + - 'app/views/invites/_form.html.haml' + - 'app/views/layouts/application.html.haml' + - 'app/views/layouts/error.html.haml' + - 'app/views/notification_mailer/_status.html.haml' + - 'app/views/settings/applications/_fields.html.haml' + - 'app/views/settings/imports/show.html.haml' + - 'app/views/settings/preferences/appearance/show.html.haml' + - 'app/views/settings/preferences/other/show.html.haml' + - 'app/views/statuses/_detailed_status.html.haml' + - 'app/views/statuses/_poll.html.haml' + - 'app/views/statuses/show.html.haml' + - 'app/views/statuses_cleanup/show.html.haml' + - 'app/views/user_mailer/warning.html.haml' + + # Offense count: 913 + LineLength: + enabled: false + + # Offense count: 22 + UnnecessaryStringOutput: + exclude: + - 'app/views/accounts/show.html.haml' + - 'app/views/admin/custom_emojis/_custom_emoji.html.haml' + - 'app/views/admin/relays/_relay.html.haml' + - 'app/views/admin/rules/_rule.html.haml' + - 'app/views/admin/statuses/index.html.haml' + - 'app/views/auth/registrations/_sessions.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + - 'app/views/notification_mailer/_status.html.haml' + - 'app/views/settings/two_factor_authentication_methods/index.html.haml' + - 'app/views/statuses/_detailed_status.html.haml' + - 'app/views/statuses/_poll.html.haml' + - 'app/views/statuses/_simple_status.html.haml' + - 'app/views/user_mailer/suspicious_sign_in.html.haml' + - 'app/views/user_mailer/webauthn_credential_added.html.haml' + - 'app/views/user_mailer/webauthn_credential_deleted.html.haml' + - 'app/views/user_mailer/welcome.html.haml' + + # Offense count: 3 + ViewLength: + exclude: + - 'app/views/admin/accounts/show.html.haml' + - 'app/views/admin/reports/show.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + + # Offense count: 41 + InstanceVariables: + exclude: + - 'app/views/admin/reports/_actions.html.haml' + - 'app/views/admin/roles/_form.html.haml' + - 'app/views/admin/webhooks/_form.html.haml' + - 'app/views/auth/registrations/_sessions.html.haml' + - 'app/views/auth/registrations/_status.html.haml' + - 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml' + - 'app/views/authorize_interactions/_post_follow_actions.html.haml' + - 'app/views/invites/_form.html.haml' + - 'app/views/relationships/_account.html.haml' + - 'app/views/shared/_og.html.haml' + - 'app/views/statuses/_status.html.haml' + + # Offense count: 6 + ConsecutiveSilentScripts: + exclude: + - 'app/views/admin/settings/shared/_links.html.haml' + - 'app/views/settings/login_activities/_login_activity.html.haml' + - 'app/views/statuses/_poll.html.haml' + + # Offense count: 3 + IdNames: + exclude: + - 'app/views/authorize_interactions/error.html.haml' + - 'app/views/oauth/authorizations/error.html.haml' + - 'app/views/shared/_error_messages.html.haml' diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..d2ae35e84 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.nvmrc b/.nvmrc index 030fcd56b..59ea99ee6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.19 +16.20 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 27d778edf..ceade6e58 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,7 @@ +# Can be removed once all rules are addressed or moved to this file as documented overrides inherit_from: .rubocop_todo.yml +# Used for merging with exclude lists with .rubocop_todo.yml inherit_mode: merge: - Exclude @@ -8,32 +10,34 @@ require: - rubocop-rails - rubocop-rspec - rubocop-performance + - rubocop-capybara AllCops: - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.0 # Set to minimum supported version of CI DisplayCopNames: true DisplayStyleGuide: true ExtraDetails: true UseCache: true CacheRootDirectory: tmp - NewCops: enable + NewCops: enable # Opt-in to newly added rules Exclude: - db/schema.rb - - 'app/views/**/*' - - 'config/**/*' - 'bin/*' - 'Rakefile' - 'node_modules/**/*' - 'Vagrantfile' - 'vendor/**/*' - - 'lib/json_ld/*' + - 'lib/json_ld/*' # Generated files - 'lib/templates/**/*' +# Reason: Prefer Hashes without extreme indentation +# https://docs.rubocop.org/rubocop/cops_layout.html#layoutfirsthashelementindentation Layout/FirstHashElementIndentation: EnforcedStyle: consistent +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength Layout/LineLength: - Max: 140 # RuboCop default 120 AllowedPatterns: # Allow comments to be long lines - !ruby/regexp / \# .*$/ @@ -43,83 +47,223 @@ Layout/LineLength: - db/*migrate/**/* - db/seeds/**/* +# Reason: +# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier Lint/UselessAccessModifier: ContextCreatingMethods: - class_methods +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize Metrics/AbcSize: - Max: 34 # RuboCop default 17 Exclude: - 'lib/**/*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: - Max: 55 # Default 25 - CountAsOne: [array, heredoc] + CountAsOne: ['array', 'hash', 'heredoc', 'method_call'] Exclude: + - 'config/routes.rb' - 'lib/mastodon/*_cli.rb' + - 'lib/tasks/*.rake' + - 'app/models/concerns/account_associations.rb' + - 'app/models/concerns/account_interactions.rb' + - 'app/models/concerns/ldap_authenticable.rb' + - 'app/models/concerns/omniauthable.rb' + - 'app/models/concerns/pam_authenticable.rb' + - 'app/models/concerns/remotable.rb' + - 'app/services/suspend_account_service.rb' + - 'app/services/unsuspend_account_service.rb' + - 'app/views/accounts/show.rss.ruby' + - 'app/views/tags/show.rss.ruby' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/doorkeeper.rb' + - 'config/initializers/omniauth.rb' + - 'config/initializers/simple_form.rb' + - 'config/navigation.rb' + - 'config/routes.rb' + - 'config/routes/*.rb' + - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' + - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' + - 'lib/paperclip/gif_transcoder.rb' +# Reason: +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting Metrics/BlockNesting: Exclude: - 'lib/mastodon/*_cli.rb' +# Reason: Some Excluded files would be candidates for refactoring but not currently addressed +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength Metrics/ClassLength: - Max: 500 # Default 100 - CountAsOne: [array, heredoc] + CountAsOne: ['array', 'hash', 'heredoc', 'method_call'] Exclude: - 'lib/mastodon/*_cli.rb' + - 'app/controllers/admin/accounts_controller.rb' + - 'app/controllers/api/base_controller.rb' + - 'app/controllers/api/v1/admin/accounts_controller.rb' + - 'app/controllers/application_controller.rb' + - 'app/controllers/auth/registrations_controller.rb' + - 'app/controllers/auth/sessions_controller.rb' + - 'app/lib/activitypub/activity.rb' + - 'app/lib/activitypub/activity/create.rb' + - 'app/lib/activitypub/tag_manager.rb' + - 'app/lib/feed_manager.rb' + - 'app/lib/link_details_extractor.rb' + - 'app/lib/request.rb' + - 'app/lib/text_formatter.rb' + - 'app/lib/user_settings_decorator.rb' + - 'app/mailers/user_mailer.rb' + - 'app/models/account.rb' + - 'app/models/admin/account_action.rb' + - 'app/models/form/account_batch.rb' + - 'app/models/media_attachment.rb' + - 'app/models/status.rb' + - 'app/models/tag.rb' + - 'app/models/user.rb' + - 'app/serializers/activitypub/actor_serializer.rb' + - 'app/serializers/activitypub/note_serializer.rb' + - 'app/serializers/rest/status_serializer.rb' + - 'app/services/account_search_service.rb' + - 'app/services/activitypub/process_account_service.rb' + - 'app/services/activitypub/process_status_update_service.rb' + - 'app/services/backup_service.rb' + - 'app/services/bulk_import_service.rb' + - 'app/services/delete_account_service.rb' + - 'app/services/fan_out_on_write_service.rb' + - 'app/services/fetch_link_card_service.rb' + - 'app/services/import_service.rb' + - 'app/services/notify_service.rb' + - 'app/services/post_status_service.rb' + - 'app/services/update_status_service.rb' + - 'lib/paperclip/color_extractor.rb' +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity Metrics/CyclomaticComplexity: - Max: 12 # Default 7 Exclude: - lib/mastodon/*cli*.rb - db/*migrate/**/* +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength Metrics/MethodLength: - Max: 25 # RuboCop default 10 CountAsOne: [array, heredoc] Exclude: - 'lib/mastodon/*_cli.rb' +# Reason: +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength Metrics/ModuleLength: - Max: 200 # Default 100 CountAsOne: [array, heredoc] -Metrics/PerceivedComplexity: - Max: 16 # RuboCop default 8 +# 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 +# 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' +# Reason: Some single letter camel case files shouldn't be split +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath +RSpec/FilePath: + CustomTransform: + ActivityPub: activitypub # Ignore the snake_case due to the amount of files to rename + DeepL: deepl + FetchOEmbedService: fetch_oembed_service + JsonLdHelper: jsonld_helper + OEmbedController: oembed_controller + OStatus: ostatus + NodeInfoController: nodeinfo_controller # NodeInfo isn't snake_cased for any of the instances + Exclude: + - 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder + - 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder + - 'spec/controllers/concerns/account_controller_concern_spec.rb' # Concerns describe ApplicationController and don't fit naming + - 'spec/controllers/concerns/export_controller_concern_spec.rb' + - 'spec/controllers/concerns/localized_spec.rb' + - 'spec/controllers/concerns/rate_limit_headers_spec.rb' + - 'spec/controllers/concerns/signature_verification_spec.rb' + - 'spec/controllers/concerns/user_tracking_concern_spec.rb' + +# Reason: +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject +RSpec/NamedSubject: + EnforcedStyle: named_only + +# Reason: Prevailing style choice +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnottonot RSpec/NotToNot: EnforcedStyle: to_not +# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus +# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus RSpec/Rails/HttpStatus: EnforcedStyle: numeric +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren +Style/ClassAndModuleChildren: + Enabled: false + +# Reason: Classes mostly self-document with their names +# https://docs.rubocop.org/rubocop/cops_style.html#styledocumentation +Style/Documentation: + Enabled: false + +# Reason: Enforce modern Ruby style +# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax Style/HashSyntax: EnforcedStyle: ruby19_no_mixed_keys +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals Style/NumericLiterals: AllowedPatterns: - \d{4}_\d{2}_\d{2}_\d{6} # For DB migration date version number readability +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#stylepercentliteraldelimiters Style/PercentLiteralDelimiters: PreferredDelimiters: '%i': '()' '%w': '()' +# Reason: Prefer less indentation in conditional assignments +# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantbegin +Style/RedundantBegin: + Enabled: false + +# Reason: Overridden to reduce implicit StandardError rescues +# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror Style/RescueStandardError: EnforcedStyle: implicit +# Reason: Originally disabled for CodeClimate, and no config consensus has been found +# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray +Style/SymbolArray: + Enabled: false + +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainarrayliteral Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: 'comma' +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8cd4afa5a..77caad492 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,12 +1,11 @@ # This configuration was generated by -# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit` -# on 2023-02-19 06:22:09 UTC using RuboCop version 1.45.1. +# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` +# using RuboCop version 1.50.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 15 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. # Include: **/*.gemfile, **/Gemfile, **/gems.rb @@ -14,14 +13,41 @@ Bundler/OrderedGems: Exclude: - 'Gemfile' -# Offense count: 581 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. -# URISchemes: http, https -Layout/LineLength: - Enabled: false +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_argument, with_fixed_indentation +Layout/ArgumentAlignment: + Exclude: + - 'config/initializers/cors.rb' + - 'config/initializers/session_store.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'config/boot.rb' + - 'config/environments/production.rb' + - 'config/initializers/rack_attack.rb' + - 'config/routes.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. +Layout/LeadingCommentSpace: + Exclude: + - 'config/application.rb' + - 'config/initializers/omniauth.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_no_space, require_space +Layout/SpaceInLambdaLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/content_security_policy.rb' -# Offense count: 14 # Configuration parameters: AllowedMethods, AllowedPatterns. Lint/AmbiguousBlockAssociation: Exclude: @@ -35,44 +61,13 @@ Lint/AmbiguousBlockAssociation: - 'spec/services/unsuspend_account_service_spec.rb' - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' -# Offense count: 15 -# 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/lib/settings/extend_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - -# Offense count: 5 -# 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' - -# Offense count: 42 # 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/conversation_mute_fabricator.rb' - - 'spec/fabricators/import_fabricator.rb' - - 'spec/fabricators/setting_fabricator.rb' - 'spec/fabricators/system_key_fabricator.rb' - - 'spec/fabricators/web_setting_fabricator.rb' - 'spec/helpers/admin/action_logs_helper_spec.rb' - 'spec/lib/activitypub/adapter_spec.rb' - 'spec/models/account_alias_spec.rb' @@ -93,7 +88,6 @@ Lint/EmptyBlock: - 'spec/models/list_spec.rb' - 'spec/models/login_activity_spec.rb' - 'spec/models/mute_spec.rb' - - 'spec/models/one_time_key_spec.rb' - 'spec/models/preview_card_spec.rb' - 'spec/models/preview_card_trend_spec.rb' - 'spec/models/relay_spec.rb' @@ -107,33 +101,33 @@ Lint/EmptyBlock: - 'spec/models/user_role_spec.rb' - 'spec/models/web/setting_spec.rb' -# Offense count: 1 -# Configuration parameters: AllowComments. -Lint/EmptyClass: - Exclude: - - 'spec/controllers/api/base_controller_spec.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Lint/NonDeterministicRequireOrder: - Exclude: - - 'spec/rails_helper.rb' - -# Offense count: 1 Lint/NonLocalExitFromIterator: Exclude: - 'app/helpers/jsonld_helper.rb' -# Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). Lint/OrAssignmentToConstant: Exclude: - 'lib/sanitize_ext/sanitize_config.rb' -# Offense count: 33 +# This cop supports safe autocorrection (--autocorrect). +Lint/SendWithMixinArgument: + Exclude: + - 'config/application.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/doorkeeper.rb' + - 'config/initializers/paperclip.rb' + - 'config/initializers/simple_form.rb' + Lint/UselessAssignment: Exclude: - 'app/services/activitypub/process_status_update_service.rb' + - 'config/initializers/omniauth.rb' - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' - 'spec/controllers/api/v1/bookmarks_controller_spec.rb' @@ -151,53 +145,46 @@ Lint/UselessAssignment: - 'spec/services/resolve_url_service_spec.rb' - 'spec/views/statuses/show.html.haml_spec.rb' -# Offense count: 3 # Configuration parameters: CheckForMethodsWithNoSideEffects. Lint/Void: Exclude: - 'spec/services/resolve_account_service_spec.rb' -# Offense count: 66 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Enabled: false + Max: 150 + Exclude: + - 'app/serializers/initial_state_serializer.rb' -# Offense count: 10 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. # AllowedMethods: refine Metrics/BlockLength: Exclude: - - 'app/models/concerns/account_interactions.rb' - - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' - - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' - - 'lib/tasks/branding.rake' - - 'lib/tasks/mastodon.rake' - - 'lib/tasks/repo.rake' - - 'lib/tasks/tests.rake' + - 'app/models/concerns/status_safe_reblog_insert.rb' -# Offense count: 1 # Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: Exclude: - 'lib/tasks/mastodon.rake' -# Offense count: 39 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Enabled: false + Max: 25 -# Offense count: 35 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: - Enabled: false + Max: 58 -# Offense count: 1 # Configuration parameters: CountComments, Max, CountAsOne. Metrics/ModuleLength: Exclude: + - 'app/controllers/concerns/signature_verification.rb' + - 'app/helpers/application_helper.rb' + - 'app/helpers/jsonld_helper.rb' - 'app/models/concerns/account_interactions.rb' + - 'app/models/concerns/has_user_settings.rb' + - 'lib/sanitize_ext/sanitize_config.rb' -# Offense count: 5 # Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. Metrics/ParameterLists: Exclude: @@ -206,32 +193,21 @@ Metrics/ParameterLists: - 'app/services/activitypub/fetch_remote_actor_service.rb' - 'app/services/activitypub/fetch_remote_status_service.rb' -# Offense count: 16 -# Configuration parameters: AllowedMethods, AllowedPatterns, Max. +# Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Exclude: - - 'app/helpers/jsonld_helper.rb' - - 'app/lib/feed_manager.rb' - - 'app/lib/status_cache_hydrator.rb' - - 'app/lib/user_settings_decorator.rb' - - 'app/models/trends/links.rb' - - 'app/services/activitypub/fetch_remote_key_service.rb' - - 'app/services/activitypub/fetch_remote_status_service.rb' - - 'app/services/activitypub/process_account_service.rb' - - 'app/services/fetch_link_card_service.rb' - - 'app/services/fetch_oembed_service.rb' - - 'app/services/process_mentions_service.rb' - - 'app/services/resolve_account_service.rb' - - 'lib/mastodon/accounts_cli.rb' - - 'lib/mastodon/domains_cli.rb' - - 'lib/mastodon/maintenance_cli.rb' + Max: 28 -# Offense count: 1 Naming/AccessorMethodName: Exclude: - 'app/controllers/auth/sessions_controller.rb' -# Offense count: 7 +# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. +# CheckDefinitionPathHierarchyRoots: lib, spec, test, src +# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS +Naming/FileName: + Exclude: + - 'config/locales/sr-Latn.rb' + # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: @@ -242,11 +218,11 @@ Naming/MemoizedInstanceVariableName: - 'app/lib/activitypub/activity.rb' - 'app/services/resolve_url_service.rb' - 'app/services/search_service.rb' + - 'config/initializers/rack_attack.rb' -# Offense count: 50 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer -# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339 +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 Naming/VariableNumber: Exclude: - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' @@ -263,111 +239,6 @@ Naming/VariableNumber: - 'spec/models/user_spec.rb' - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' -# Offense count: 12 -# 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' - - 'lib/mastodon/media_cli.rb' - -# Offense count: 4 -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/Count: - Exclude: - - 'app/lib/importer/accounts_index_importer.rb' - - 'app/lib/importer/tags_index_importer.rb' - -# Offense count: 10 -# 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' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/DeleteSuffix: - Exclude: - - 'lib/tasks/repo.rake' - -# Offense count: 19 -# 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' - -# Offense count: 7 -Performance/MethodObjectAsBlock: - Exclude: - - 'app/models/account_suggestions/source.rb' - - 'spec/models/export_spec.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/RedundantEqualityComparisonBlock: - Exclude: - - 'spec/requests/link_headers_spec.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/StartWith: - Exclude: - - 'app/lib/extractor.rb' - -# Offense count: 4 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: OnlySumOrWithInitialValue. -Performance/Sum: - Exclude: - - 'app/lib/activity_tracker.rb' - - 'app/models/trends/history.rb' - - 'app/workers/scheduler/accounts_statuses_cleanup_scheduler.rb' - - 'lib/paperclip/color_extractor.rb' - -# Offense count: 15 -# 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' - -# Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). Performance/UnfreezeString: Exclude: @@ -376,7 +247,6 @@ Performance/UnfreezeString: - 'app/validators/status_length_validator.rb' - 'lib/tasks/mastodon.rake' -# Offense count: 27 RSpec/AnyInstance: Exclude: - 'spec/controllers/activitypub/inboxes_controller_spec.rb' @@ -397,123 +267,6 @@ RSpec/AnyInstance: - 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb' -# Offense count: 1 -RSpec/BeforeAfterAll: - Exclude: - - 'spec/requests/localization_spec.rb' - -# Offense count: 558 -# 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/plain_text_formatter_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' - -# Offense count: 339 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle. # SupportedStyles: described_class, explicit @@ -555,7 +308,6 @@ RSpec/DescribedClass: - 'spec/models/user_spec.rb' - 'spec/policies/account_moderation_note_policy_spec.rb' - 'spec/presenters/account_relationships_presenter_spec.rb' - - 'spec/presenters/instance_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' @@ -588,7 +340,6 @@ RSpec/DescribedClass: - 'spec/services/update_account_service_spec.rb' - 'spec/validators/note_length_validator_spec.rb' -# Offense count: 32 # This cop supports unsafe autocorrection (--autocorrect-all). RSpec/EmptyExampleGroup: Exclude: @@ -611,7 +362,6 @@ RSpec/EmptyExampleGroup: - 'spec/models/list_spec.rb' - 'spec/models/login_activity_spec.rb' - 'spec/models/mute_spec.rb' - - 'spec/models/one_time_key_spec.rb' - 'spec/models/preview_card_spec.rb' - 'spec/models/preview_card_trend_spec.rb' - 'spec/models/relay_spec.rb' @@ -625,12 +375,10 @@ RSpec/EmptyExampleGroup: - 'spec/models/web/setting_spec.rb' - 'spec/services/unmute_service_spec.rb' -# Offense count: 178 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 22 -# Offense count: 21 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: method_call, block @@ -648,81 +396,12 @@ RSpec/ExpectChange: - 'spec/services/unsuspend_account_service_spec.rb' - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' -# Offense count: 5 RSpec/ExpectInHook: Exclude: - 'spec/controllers/api/v1/media_controller_spec.rb' - 'spec/controllers/settings/applications_controller_spec.rb' - 'spec/lib/status_filter_spec.rb' -# Offense count: 61 -# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. -# Include: **/*_spec*rb*, **/spec/**/* -RSpec/FilePath: - Exclude: - - 'spec/config/initializers/rack_attack_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/change_email_controller_spec.rb' - - 'spec/controllers/admin/users/roles_controller.rb' - - 'spec/controllers/api/oembed_controller_spec.rb' - - 'spec/controllers/concerns/account_controller_concern_spec.rb' - - 'spec/controllers/concerns/export_controller_concern_spec.rb' - - 'spec/controllers/concerns/localized_spec.rb' - - 'spec/controllers/concerns/rate_limit_headers_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/controllers/concerns/user_tracking_concern_spec.rb' - - 'spec/controllers/well_known/nodeinfo_controller_spec.rb' - - 'spec/helpers/admin/action_log_helper_spec.rb' - - 'spec/helpers/jsonld_helper_spec.rb' - - 'spec/lib/activitypub/activity/accept_spec.rb' - - 'spec/lib/activitypub/activity/add_spec.rb' - - 'spec/lib/activitypub/activity/announce_spec.rb' - - 'spec/lib/activitypub/activity/block_spec.rb' - - 'spec/lib/activitypub/activity/create_spec.rb' - - 'spec/lib/activitypub/activity/delete_spec.rb' - - 'spec/lib/activitypub/activity/flag_spec.rb' - - 'spec/lib/activitypub/activity/follow_spec.rb' - - 'spec/lib/activitypub/activity/like_spec.rb' - - 'spec/lib/activitypub/activity/move_spec.rb' - - 'spec/lib/activitypub/activity/reject_spec.rb' - - 'spec/lib/activitypub/activity/remove_spec.rb' - - 'spec/lib/activitypub/activity/undo_spec.rb' - - 'spec/lib/activitypub/activity/update_spec.rb' - - 'spec/lib/activitypub/adapter_spec.rb' - - 'spec/lib/activitypub/dereferencer_spec.rb' - - 'spec/lib/activitypub/linked_data_signature_spec.rb' - - 'spec/lib/activitypub/tag_manager_spec.rb' - - 'spec/lib/ostatus/tag_manager_spec.rb' - - 'spec/lib/sanitize_config_spec.rb' - - 'spec/serializers/activitypub/note_serializer_spec.rb' - - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' - - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' - - 'spec/services/activitypub/fetch_featured_tags_collection_service_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/activitypub/fetch_remote_status_service_spec.rb' - - 'spec/services/activitypub/fetch_replies_service_spec.rb' - - 'spec/services/activitypub/process_account_service_spec.rb' - - 'spec/services/activitypub/process_collection_service_spec.rb' - - 'spec/services/activitypub/process_status_update_service_spec.rb' - - 'spec/services/activitypub/synchronize_followers_service_spec.rb' - - 'spec/services/fetch_oembed_service_spec.rb' - - 'spec/services/remove_from_follwers_service_spec.rb' - - 'spec/workers/activitypub/delivery_worker_spec.rb' - - 'spec/workers/activitypub/distribute_poll_update_worker_spec.rb' - - 'spec/workers/activitypub/distribution_worker_spec.rb' - - 'spec/workers/activitypub/fetch_replies_worker_spec.rb' - - 'spec/workers/activitypub/move_distribution_worker_spec.rb' - - 'spec/workers/activitypub/processing_worker_spec.rb' - - 'spec/workers/activitypub/status_update_distribution_worker_spec.rb' - - 'spec/workers/activitypub/update_distribution_worker_spec.rb' - -# Offense count: 16 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: implicit, each, example @@ -738,12 +417,10 @@ RSpec/HookArgument: - 'spec/services/import_service_spec.rb' - 'spec/spec_helper.rb' -# Offense count: 101 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: - 'spec/controllers/api/v1/streaming_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - 'spec/controllers/auth/confirmations_controller_spec.rb' - 'spec/controllers/auth/passwords_controller_spec.rb' - 'spec/controllers/auth/sessions_controller_spec.rb' @@ -753,7 +430,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' @@ -761,20 +437,6 @@ RSpec/InstanceVariable: - 'spec/services/search_service_spec.rb' - 'spec/services/unblock_domain_service_spec.rb' -# Offense count: 15 -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/lib/settings/extend_spec.rb' - - 'spec/models/concerns/remotable_spec.rb' - -# Offense count: 108 RSpec/LetSetup: Exclude: - 'spec/controllers/admin/accounts_controller_spec.rb' @@ -800,7 +462,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' @@ -815,6 +477,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' @@ -827,7 +490,6 @@ RSpec/LetSetup: - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' -# Offense count: 7 RSpec/MessageChain: Exclude: - 'spec/controllers/api/v1/media_controller_spec.rb' @@ -835,7 +497,6 @@ RSpec/MessageChain: - 'spec/models/session_activation_spec.rb' - 'spec/models/setting_spec.rb' -# Offense count: 47 # Configuration parameters: EnforcedStyle. # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -858,7 +519,6 @@ RSpec/MessageSpies: - 'spec/spec_helper.rb' - 'spec/validators/status_length_validator_spec.rb' -# Offense count: 35 RSpec/MissingExampleGroupArgument: Exclude: - 'spec/controllers/accounts_controller_spec.rb' @@ -869,7 +529,6 @@ RSpec/MissingExampleGroupArgument: - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb' - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb' - 'spec/controllers/api/v1/statuses_controller_spec.rb' - - 'spec/controllers/application_controller_spec.rb' - 'spec/controllers/auth/registrations_controller_spec.rb' - 'spec/features/log_in_spec.rb' - 'spec/lib/activitypub/activity/undo_spec.rb' @@ -884,232 +543,17 @@ RSpec/MissingExampleGroupArgument: - 'spec/services/notify_service_spec.rb' - 'spec/services/process_mentions_service_spec.rb' -# Offense count: 599 RSpec/MultipleExpectations: Max: 19 -# Offense count: 442 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: Max: 21 -# Offense count: 7 -# 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' - -# Offense count: 1407 -# Configuration parameters: EnforcedStyle, IgnoreSharedExamples. -# SupportedStyles: always, named_only -RSpec/NamedSubject: - Exclude: - - 'spec/controllers/admin/account_moderation_notes_controller_spec.rb' - - 'spec/controllers/admin/accounts_controller_spec.rb' - - 'spec/controllers/admin/confirmations_controller_spec.rb' - - 'spec/controllers/admin/custom_emojis_controller_spec.rb' - - 'spec/controllers/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/admin/instances_controller_spec.rb' - - 'spec/controllers/admin/invites_controller_spec.rb' - - 'spec/controllers/admin/report_notes_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/notes_controller_spec.rb' - - 'spec/controllers/api/v1/accounts/pins_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb' - - 'spec/controllers/auth/passwords_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/controllers/home_controller_spec.rb' - - 'spec/controllers/invites_controller_spec.rb' - - 'spec/controllers/oauth/authorizations_controller_spec.rb' - - 'spec/controllers/oauth/authorized_applications_controller_spec.rb' - - 'spec/controllers/relationships_controller_spec.rb' - - 'spec/controllers/settings/featured_tags_controller_spec.rb' - - 'spec/controllers/settings/migrations_controller_spec.rb' - - 'spec/controllers/settings/sessions_controller_spec.rb' - - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' - - 'spec/controllers/well_known/webfinger_controller_spec.rb' - - 'spec/features/log_in_spec.rb' - - 'spec/features/profile_spec.rb' - - 'spec/lib/activitypub/activity/accept_spec.rb' - - 'spec/lib/activitypub/activity/add_spec.rb' - - 'spec/lib/activitypub/activity/announce_spec.rb' - - 'spec/lib/activitypub/activity/block_spec.rb' - - 'spec/lib/activitypub/activity/create_spec.rb' - - 'spec/lib/activitypub/activity/delete_spec.rb' - - 'spec/lib/activitypub/activity/flag_spec.rb' - - 'spec/lib/activitypub/activity/follow_spec.rb' - - 'spec/lib/activitypub/activity/like_spec.rb' - - 'spec/lib/activitypub/activity/move_spec.rb' - - 'spec/lib/activitypub/activity/reject_spec.rb' - - 'spec/lib/activitypub/activity/remove_spec.rb' - - 'spec/lib/activitypub/activity/undo_spec.rb' - - 'spec/lib/activitypub/activity/update_spec.rb' - - 'spec/lib/activitypub/adapter_spec.rb' - - 'spec/lib/activitypub/dereferencer_spec.rb' - - 'spec/lib/activitypub/linked_data_signature_spec.rb' - - 'spec/lib/activitypub/tag_manager_spec.rb' - - 'spec/lib/connection_pool/shared_connection_pool_spec.rb' - - 'spec/lib/connection_pool/shared_timed_stack_spec.rb' - - 'spec/lib/delivery_failure_tracker_spec.rb' - - 'spec/lib/emoji_formatter_spec.rb' - - 'spec/lib/entity_cache_spec.rb' - - 'spec/lib/fast_ip_map_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - - 'spec/lib/hashtag_normalizer_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/plain_text_formatter_spec.rb' - - 'spec/lib/request_pool_spec.rb' - - 'spec/lib/request_spec.rb' - - 'spec/lib/sanitize_config_spec.rb' - - 'spec/lib/status_finder_spec.rb' - - 'spec/lib/status_reach_finder_spec.rb' - - 'spec/lib/suspicious_sign_in_detector_spec.rb' - - 'spec/lib/text_formatter_spec.rb' - - 'spec/lib/vacuum/access_tokens_vacuum_spec.rb' - - 'spec/lib/vacuum/backups_vacuum_spec.rb' - - 'spec/lib/vacuum/feeds_vacuum_spec.rb' - - 'spec/lib/vacuum/media_attachments_vacuum_spec.rb' - - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' - - 'spec/lib/vacuum/statuses_vacuum_spec.rb' - - 'spec/lib/vacuum/system_keys_vacuum_spec.rb' - - 'spec/models/account/field_spec.rb' - - 'spec/models/account_migration_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/account_statuses_cleanup_policy_spec.rb' - - 'spec/models/account_statuses_filter_spec.rb' - - 'spec/models/admin/account_action_spec.rb' - - 'spec/models/canonical_email_block_spec.rb' - - 'spec/models/concerns/account_interactions_spec.rb' - - 'spec/models/custom_emoji_filter_spec.rb' - - 'spec/models/custom_emoji_spec.rb' - - 'spec/models/follow_spec.rb' - - 'spec/models/home_feed_spec.rb' - - 'spec/models/media_attachment_spec.rb' - - 'spec/models/notification_spec.rb' - - 'spec/models/public_feed_spec.rb' - - 'spec/models/relationship_filter_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/tag_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/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/status_policy_spec.rb' - - 'spec/policies/tag_policy_spec.rb' - - 'spec/policies/user_policy_spec.rb' - - 'spec/presenters/familiar_followers_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/account_search_service_spec.rb' - - 'spec/services/account_statuses_cleanup_service_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_status_service_spec.rb' - - 'spec/services/activitypub/fetch_replies_service_spec.rb' - - 'spec/services/activitypub/process_account_service_spec.rb' - - 'spec/services/activitypub/process_collection_service_spec.rb' - - 'spec/services/activitypub/process_status_update_service_spec.rb' - - 'spec/services/after_block_domain_from_account_service_spec.rb' - - 'spec/services/after_block_service_spec.rb' - - 'spec/services/app_sign_up_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/delete_account_service_spec.rb' - - 'spec/services/fan_out_on_write_service_spec.rb' - - 'spec/services/favourite_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/fetch_resource_service_spec.rb' - - 'spec/services/follow_service_spec.rb' - - 'spec/services/import_service_spec.rb' - - 'spec/services/mute_service_spec.rb' - - 'spec/services/notify_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/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/suspend_account_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - - 'spec/services/unblock_domain_service_spec.rb' - - 'spec/services/unblock_service_spec.rb' - - 'spec/services/unfollow_service_spec.rb' - - 'spec/services/unsuspend_account_service_spec.rb' - - 'spec/services/update_account_service_spec.rb' - - 'spec/services/update_status_service_spec.rb' - - 'spec/services/verify_link_service_spec.rb' - - 'spec/validators/blacklisted_email_validator_spec.rb' - - 'spec/validators/email_mx_validator_spec.rb' - - 'spec/validators/note_length_validator_spec.rb' - - 'spec/validators/reaction_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/workers/activitypub/delivery_worker_spec.rb' - - 'spec/workers/activitypub/distribute_poll_update_worker_spec.rb' - - 'spec/workers/activitypub/distribution_worker_spec.rb' - - 'spec/workers/activitypub/fetch_replies_worker_spec.rb' - - 'spec/workers/activitypub/move_distribution_worker_spec.rb' - - 'spec/workers/activitypub/processing_worker_spec.rb' - - 'spec/workers/activitypub/status_update_distribution_worker_spec.rb' - - 'spec/workers/activitypub/update_distribution_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/move_worker_spec.rb' - - 'spec/workers/publish_scheduled_announcement_worker_spec.rb' - - 'spec/workers/publish_scheduled_status_worker_spec.rb' - - 'spec/workers/refollow_worker_spec.rb' - - 'spec/workers/regeneration_worker_spec.rb' - - 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb' - - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' - - 'spec/workers/unfollow_follow_worker_spec.rb' - - 'spec/workers/web/push_notification_worker_spec.rb' - -# Offense count: 552 # Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 6 -# Offense count: 2 # Configuration parameters: AllowedPatterns. # AllowedPatterns: ^expect_, ^assert_ RSpec/NoExpectationExample: @@ -1117,13 +561,10 @@ RSpec/NoExpectationExample: - 'spec/controllers/auth/registrations_controller_spec.rb' - 'spec/services/precompute_feed_service_spec.rb' -# Offense count: 3 RSpec/PendingWithoutReason: Exclude: - 'spec/models/account_spec.rb' - - 'spec/support/examples/lib/settings/scoped_settings.rb' -# Offense count: 9 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers. # SupportedStyles: inflected, explicit @@ -1133,225 +574,6 @@ RSpec/PredicateMatcher: - 'spec/models/user_spec.rb' - 'spec/services/post_status_service_spec.rb' -# Offense count: 180 -# 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/helpers/statuses_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/appeal_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_category_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_allow_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/ip_block_spec.rb' - - 'spec/models/list_account_spec.rb' - - 'spec/models/list_spec.rb' - - 'spec/models/login_activity_spec.rb' - - 'spec/models/marker_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/one_time_key_spec.rb' - - 'spec/models/poll_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/rule_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_edit_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' - -# Offense count: 6 -RSpec/RepeatedExample: - Exclude: - - 'spec/policies/status_policy_spec.rb' - -# Offense count: 6 -RSpec/RepeatedExampleGroupBody: - Exclude: - - 'spec/controllers/statuses_controller_spec.rb' - -# Offense count: 4 -RSpec/RepeatedExampleGroupDescription: - Exclude: - - 'spec/controllers/admin/reports/actions_controller_spec.rb' - - 'spec/policies/report_note_policy_spec.rb' - -# Offense count: 12 -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' - - 'spec/services/fetch_resource_service_spec.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -RSpec/SharedContext: - Exclude: - - 'spec/services/unsuspend_account_service_spec.rb' - -# Offense count: 16 RSpec/StubbedMock: Exclude: - 'spec/controllers/api/base_controller_spec.rb' @@ -1363,7 +585,6 @@ RSpec/StubbedMock: - 'spec/lib/webfinger_resource_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb' -# Offense count: 22 RSpec/SubjectDeclaration: Exclude: - 'spec/controllers/admin/domain_blocks_controller_spec.rb' @@ -1388,13 +609,11 @@ RSpec/SubjectDeclaration: - 'spec/policies/user_policy_spec.rb' - 'spec/services/activitypub/process_account_service_spec.rb' -# Offense count: 5 RSpec/SubjectStub: Exclude: - 'spec/services/unallow_domain_service_spec.rb' - 'spec/validators/blacklisted_email_validator_spec.rb' -# Offense count: 119 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -1407,7 +626,6 @@ RSpec/VerifiedDoubles: - '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' @@ -1435,57 +653,11 @@ RSpec/VerifiedDoubles: - 'spec/workers/feed_insert_worker_spec.rb' - 'spec/workers/regeneration_worker_spec.rb' -# Offense count: 19 -# 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' - -# Offense count: 7 -# 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' - -# Offense count: 4 # 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' -# Offense count: 35 # Configuration parameters: Database, Include. # SupportedDatabases: mysql, postgresql # Include: db/migrate/*.rb @@ -1523,25 +695,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' -# Offense count: 7 -# 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/models/user.rb' - - 'app/services/import_service.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Rails/ContentTag: - Exclude: - - 'app/helpers/application_helper.rb' - - 'app/helpers/branding_helper.rb' - -# Offense count: 8 # Configuration parameters: Include. # Include: db/migrate/*.rb Rails/CreateTableWithTimestamps: @@ -1555,90 +708,19 @@ Rails/CreateTableWithTimestamps: - 'db/migrate/20220824233535_create_status_trends.rb' - 'db/migrate/20221006061337_create_preview_card_trends.rb' -# Offense count: 4 -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/DeprecatedActiveModelErrorsMethods: - Exclude: - - 'app/validators/ed25519_key_validator.rb' - - 'app/validators/ed25519_signature_validator.rb' - - 'lib/mastodon/accounts_cli.rb' - -# Offense count: 4 # This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Severity. Rails/DuplicateAssociation: Exclude: - 'app/serializers/activitypub/collection_serializer.rb' - 'app/serializers/activitypub/note_serializer.rb' -# Offense count: 76 -# Configuration parameters: EnforcedStyle. -# SupportedStyles: slashes, arguments -Rails/FilePath: +# Configuration parameters: Include. +# Include: app/**/*.rb, config/**/*.rb, lib/**/*.rb +Rails/Exit: Exclude: - - 'app/lib/themes.rb' - - 'app/models/setting.rb' - - 'app/validators/reaction_validator.rb' - - 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb' - - 'db/migrate/20170918125918_ids_to_bigints.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' + - 'config/boot.rb' -# Offense count: 6 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasAndBelongsToMany: @@ -1648,7 +730,6 @@ Rails/HasAndBelongsToMany: - 'app/models/status.rb' - 'app/models/tag.rb' -# Offense count: 15 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/HasManyOrHasOneDependent: @@ -1663,78 +744,19 @@ Rails/HasManyOrHasOneDependent: - 'app/models/user.rb' - 'app/models/web/push_subscription.rb' -# Offense count: 4 -# 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' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Include. -# Include: spec/**/*, test/**/* -Rails/HttpPositionalArguments: - Exclude: - - 'spec/config/initializers/rack_attack_spec.rb' - -# Offense count: 7 -# 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' - -# Offense count: 6 Rails/I18nLocaleTexts: Exclude: - 'lib/tasks/mastodon.rake' - 'spec/helpers/flashes_helper_spec.rb' -# Offense count: 8 -# This cop supports unsafe autocorrection (--autocorrect-all). -Rails/IgnoredColumnsAssignment: - Exclude: - - 'app/models/account.rb' - - 'app/models/account_stat.rb' - - 'app/models/admin/action_log.rb' - - 'app/models/custom_filter.rb' - - 'app/models/email_domain_block.rb' - - 'app/models/report.rb' - - 'app/models/status_edit.rb' - - 'app/models/user.rb' - -# Offense count: 25 -# 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' - - 'app/models/user_ip.rb' - -# Offense count: 13 # Configuration parameters: Include. # Include: app/controllers/**/*.rb, app/mailers/**/*.rb Rails/LexicallyScopedActionFilter: Exclude: - - 'app/controllers/admin/domain_blocks_controller.rb' - - 'app/controllers/admin/email_domain_blocks_controller.rb' - 'app/controllers/auth/passwords_controller.rb' - 'app/controllers/auth/registrations_controller.rb' - 'app/controllers/auth/sessions_controller.rb' -# Offense count: 18 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/NegateInclude: Exclude: @@ -1754,15 +776,10 @@ Rails/NegateInclude: - 'app/workers/web/push_notification_worker.rb' - 'lib/paperclip/color_extractor.rb' -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Include. -# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb -Rails/Output: +Rails/OutputSafety: Exclude: - - 'lib/mastodon/ip_blocks_cli.rb' + - 'config/initializers/simple_form.rb' -# Offense count: 9 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: Include. # Include: **/Rakefile, **/*.rake @@ -1775,7 +792,6 @@ Rails/RakeEnvironment: - 'lib/tasks/repo.rake' - 'lib/tasks/statistics.rake' -# Offense count: 29 # Configuration parameters: Include. # Include: db/**/*.rb Rails/ReversibleMigration: @@ -1794,7 +810,6 @@ Rails/ReversibleMigration: - 'db/migrate/20180617162849_remove_unused_indexes.rb' - 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb' -# Offense count: 141 # Configuration parameters: ForbiddenMethods, AllowedMethods. # ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all Rails/SkipsModelValidations: @@ -1848,25 +863,31 @@ Rails/SkipsModelValidations: - 'spec/services/follow_service_spec.rb' - 'spec/services/update_account_service_spec.rb' -# Offense count: 11 -# 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' + - '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' -# Offense count: 7 -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' - -# Offense count: 4 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/UniqueValidationWithoutIndex: @@ -1876,7 +897,6 @@ Rails/UniqueValidationWithoutIndex: - 'app/models/identity.rb' - 'app/models/webauthn_credential.rb' -# Offense count: 19 # Configuration parameters: Include. # Include: app/models/**/*.rb Rails/UnusedIgnoredColumns: @@ -1890,7 +910,6 @@ Rails/UnusedIgnoredColumns: - 'app/models/status_edit.rb' - 'app/models/user.rb' -# Offense count: 61 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: exists, where @@ -1930,24 +949,12 @@ Rails/WhereExists: - 'spec/services/purge_domain_service_spec.rb' - 'spec/services/unallow_domain_service_spec.rb' -# Offense count: 5 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/CaseLikeIf: +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowOnConstant, AllowOnSelfClass. +Style/CaseEquality: Exclude: - - 'app/controllers/authorize_interactions_controller.rb' - - 'app/controllers/concerns/signature_verification.rb' - - 'app/helpers/jsonld_helper.rb' - - 'app/models/account.rb' - - 'app/services/resolve_url_service.rb' + - 'config/initializers/trusted_proxies.rb' -# Offense count: 445 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: nested, compact -Style/ClassAndModuleChildren: - Enabled: false - -# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedMethods, AllowedPatterns. # AllowedMethods: ==, equal?, eql? @@ -1956,54 +963,459 @@ Style/ClassEqualityComparison: - 'app/helpers/jsonld_helper.rb' - 'app/serializers/activitypub/outbox_serializer.rb' -# Offense count: 7 +Style/ClassVars: + Exclude: + - 'config/initializers/devise.rb' + Style/CombinableLoops: Exclude: - 'app/models/form/custom_emoji_batch.rb' - 'app/models/form/ip_block_batch.rb' -# Offense count: 5 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ConcatArrayLiterals: - Exclude: - - 'app/lib/feed_manager.rb' - -# Offense count: 1433 -# Configuration parameters: AllowedConstants. -Style/Documentation: - Enabled: false - -# Offense count: 10 # 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' + - 'config/environments/production.rb' + - 'config/initializers/2_whitelist_mode.rb' + - 'config/initializers/blacklists.rb' + - 'config/initializers/cache_buster.rb' + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/omniauth.rb' + - 'config/initializers/paperclip.rb' + - 'config/initializers/vapid.rb' - 'lib/mastodon/premailer_webpack_strategy.rb' - 'lib/mastodon/redis_config.rb' - 'lib/tasks/repo.rake' - 'spec/features/profile_spec.rb' -# Offense count: 15 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated +# AllowedMethods: redirect Style/FormatStringToken: Exclude: - 'app/models/privacy_policy.rb' + - 'config/initializers/devise.rb' - 'lib/mastodon/maintenance_cli.rb' - 'lib/paperclip/color_extractor.rb' -# Offense count: 713 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: - Enabled: false + Exclude: + - 'app/views/accounts/show.rss.ruby' + - 'app/views/tags/show.rss.ruby' + - 'app/views/well_known/host_meta/show.xml.ruby' + - 'config/application.rb' + - 'config/boot.rb' + - 'config/environment.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + - 'config/environments/test.rb' + - 'config/initializers/0_post_deployment_migrations.rb' + - 'config/initializers/active_model_serializers.rb' + - 'config/initializers/application_controller_renderer.rb' + - 'config/initializers/assets.rb' + - 'config/initializers/backtrace_silencers.rb' + - 'config/initializers/cache_logging.rb' + - 'config/initializers/chewy.rb' + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/cookies_serializer.rb' + - 'config/initializers/cors.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/doorkeeper.rb' + - 'config/initializers/fast_blank.rb' + - 'config/initializers/ffmpeg.rb' + - 'config/initializers/filter_parameter_logging.rb' + - 'config/initializers/http_client_proxy.rb' + - 'config/initializers/httplog.rb' + - 'config/initializers/inflections.rb' + - 'config/initializers/mail_delivery_job.rb' + - 'config/initializers/makara.rb' + - 'config/initializers/mime_types.rb' + - 'config/initializers/oj.rb' + - 'config/initializers/omniauth.rb' + - 'config/initializers/open_uri_redirection.rb' + - 'config/initializers/permissions_policy.rb' + - 'config/initializers/pghero.rb' + - 'config/initializers/preload_link_headers.rb' + - 'config/initializers/premailer_rails.rb' + - 'config/initializers/rack_attack_logging.rb' + - 'config/initializers/redis.rb' + - 'config/initializers/session_store.rb' + - 'config/initializers/simple_form.rb' + - 'config/initializers/stoplight.rb' + - 'config/initializers/trusted_proxies.rb' + - 'config/initializers/twitter_regex.rb' + - 'config/initializers/webauthn.rb' + - 'config/initializers/wrap_parameters.rb' + - 'config/locales/sr-Latn.rb' + - 'config/locales/sr.rb' + - 'config/puma.rb' + - 'db/migrate/20160220174730_create_accounts.rb' + - 'db/migrate/20160220211917_create_statuses.rb' + - 'db/migrate/20160221003140_create_users.rb' + - 'db/migrate/20160221003621_create_follows.rb' + - 'db/migrate/20160222122600_create_stream_entries.rb' + - 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb' + - 'db/migrate/20160223162837_add_metadata_to_statuses.rb' + - 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb' + - 'db/migrate/20160223165723_add_url_to_statuses.rb' + - 'db/migrate/20160223165855_add_url_to_accounts.rb' + - 'db/migrate/20160223171800_create_favourites.rb' + - 'db/migrate/20160224223247_create_mentions.rb' + - 'db/migrate/20160227230233_add_attachment_avatar_to_accounts.rb' + - 'db/migrate/20160305115639_add_devise_to_users.rb' + - 'db/migrate/20160306172223_create_doorkeeper_tables.rb' + - 'db/migrate/20160312193225_add_attachment_header_to_accounts.rb' + - 'db/migrate/20160314164231_add_owner_to_application.rb' + - 'db/migrate/20160316103650_add_missing_indices.rb' + - 'db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb' + - 'db/migrate/20160325130944_add_admin_to_users.rb' + - 'db/migrate/20160826155805_add_superapp_to_oauth_applications.rb' + - 'db/migrate/20160905150353_create_media_attachments.rb' + - 'db/migrate/20160919221059_add_subscription_expires_at_to_accounts.rb' + - 'db/migrate/20160920003904_remove_verify_token_from_accounts.rb' + - 'db/migrate/20160926213048_remove_owner_from_application.rb' + - 'db/migrate/20161003142332_add_confirmable_to_users.rb' + - 'db/migrate/20161003145426_create_blocks.rb' + - 'db/migrate/20161006213403_rails_settings_migration.rb' + - 'db/migrate/20161009120834_create_domain_blocks.rb' + - 'db/migrate/20161027172456_add_silenced_to_accounts.rb' + - 'db/migrate/20161104173623_create_tags.rb' + - 'db/migrate/20161105130633_create_statuses_tags_join_table.rb' + - 'db/migrate/20161116162355_add_locale_to_users.rb' + - 'db/migrate/20161119211120_create_notifications.rb' + - 'db/migrate/20161122163057_remove_unneeded_indexes.rb' + - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb' + - 'db/migrate/20161128103007_create_subscriptions.rb' + - 'db/migrate/20161130142058_add_last_successful_delivery_at_to_subscriptions.rb' + - 'db/migrate/20161130185319_add_visibility_to_statuses.rb' + - 'db/migrate/20161202132159_add_in_reply_to_account_id_to_statuses.rb' + - 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb' + - 'db/migrate/20161205214545_add_suspended_to_accounts.rb' + - 'db/migrate/20161221152630_add_hidden_to_stream_entries.rb' + - 'db/migrate/20161222201034_add_locked_to_accounts.rb' + - 'db/migrate/20161222204147_create_follow_requests.rb' + - 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb' + - 'db/migrate/20170109120109_create_web_settings.rb' + - 'db/migrate/20170112154826_migrate_settings.rb' + - 'db/migrate/20170114194937_add_application_to_statuses.rb' + - 'db/migrate/20170114203041_add_website_to_oauth_application.rb' + - 'db/migrate/20170119214911_create_preview_cards.rb' + - 'db/migrate/20170123162658_add_severity_to_domain_blocks.rb' + - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb' + - 'db/migrate/20170125145934_add_spoiler_text_to_statuses.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170205175257_remove_devices.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170214110202_create_reports.rb' + - 'db/migrate/20170217012631_add_reblog_of_id_foreign_key_to_statuses.rb' + - 'db/migrate/20170301222600_create_mutes.rb' + - 'db/migrate/20170303212857_add_last_emailed_at_to_users.rb' + - 'db/migrate/20170304202101_add_type_to_media_attachments.rb' + - 'db/migrate/20170317193015_add_search_index_to_accounts.rb' + - 'db/migrate/20170318214217_add_header_remote_url_to_accounts.rb' + - 'db/migrate/20170322021028_add_lowercase_index_to_accounts.rb' + - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb' + - 'db/migrate/20170322162804_add_search_index_to_tags.rb' + - 'db/migrate/20170330021336_add_counter_caches.rb' + - 'db/migrate/20170330163835_create_imports.rb' + - 'db/migrate/20170330164118_add_attachment_data_to_imports.rb' + - 'db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb' + - 'db/migrate/20170405112956_add_index_on_mentions_status_id.rb' + - 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb' + - 'db/migrate/20170409170753_add_last_webfingered_at_to_accounts.rb' + - 'db/migrate/20170414080609_add_devise_two_factor_backupable_to_users.rb' + - 'db/migrate/20170414132105_add_language_to_statuses.rb' + - 'db/migrate/20170418160728_add_indexes_to_reports_for_accounts.rb' + - 'db/migrate/20170423005413_add_allowed_languages_to_user.rb' + - 'db/migrate/20170424003227_create_account_domain_blocks.rb' + - 'db/migrate/20170424112722_add_status_id_index_to_statuses_tags.rb' + - 'db/migrate/20170425131920_add_media_attachment_meta.rb' + - 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb' + - 'db/migrate/20170427011934_re_add_owner_to_application.rb' + - 'db/migrate/20170506235850_create_conversations.rb' + - 'db/migrate/20170507000211_add_conversation_id_to_statuses.rb' + - 'db/migrate/20170507141759_optimize_index_subscriptions.rb' + - 'db/migrate/20170508230434_create_conversation_mutes.rb' + - 'db/migrate/20170516072309_add_index_accounts_on_uri.rb' + - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb' + - 'db/migrate/20170601210557_add_index_on_media_attachments_account_id.rb' + - 'db/migrate/20170604144747_add_foreign_keys_for_accounts.rb' + - 'db/migrate/20170606113804_change_tag_search_index_to_btree.rb' + - 'db/migrate/20170609145826_remove_default_language_from_statuses.rb' + - 'db/migrate/20170610000000_add_statuses_index_on_account_id_id.rb' + - 'db/migrate/20170623152212_create_session_activations.rb' + - 'db/migrate/20170624134742_add_description_to_session_activations.rb' + - 'db/migrate/20170625140443_add_access_token_id_to_session_activations.rb' + - 'db/migrate/20170711225116_fix_null_booleans.rb' + - 'db/migrate/20170713112503_make_tag_search_case_insensitive.rb' + - 'db/migrate/20170713175513_create_web_push_subscriptions.rb' + - 'db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb' + - 'db/migrate/20170714184731_add_domain_to_subscriptions.rb' + - 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb' + - 'db/migrate/20170718211102_add_activitypub_to_accounts.rb' + - 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb' + - 'db/migrate/20170823162448_create_status_pins.rb' + - 'db/migrate/20170824103029_add_timestamps_to_status_pins.rb' + - 'db/migrate/20170829215220_remove_status_pins_account_index.rb' + - 'db/migrate/20170901141119_truncate_preview_cards.rb' + - 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb' + - 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb' + - 'db/migrate/20170905165803_add_local_to_statuses.rb' + - 'db/migrate/20170913000752_create_site_uploads.rb' + - 'db/migrate/20170917153509_create_custom_emojis.rb' + - 'db/migrate/20170918125918_ids_to_bigints.rb' + - 'db/migrate/20170920024819_status_ids_to_timestamp_ids.rb' + - 'db/migrate/20170920032311_fix_reblogs_in_feeds.rb' + - 'db/migrate/20170924022025_ids_to_bigints2.rb' + - 'db/migrate/20170927215609_add_description_to_media_attachments.rb' + - 'db/migrate/20170928082043_create_email_domain_blocks.rb' + - 'db/migrate/20171005102658_create_account_moderation_notes.rb' + - 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb' + - 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb' + - 'db/migrate/20171010023049_add_foreign_key_to_account_moderation_notes.rb' + - 'db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb' + - 'db/migrate/20171020084748_add_visible_in_picker_to_custom_emoji.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/20171114080328_add_index_domain_to_email_domain_blocks.rb' + - 'db/migrate/20171114231651_create_lists.rb' + - 'db/migrate/20171116161857_create_list_accounts.rb' + - 'db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb' + - 'db/migrate/20171119172437_create_admin_action_logs.rb' + - 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb' + - 'db/migrate/20171125024930_create_invites.rb' + - 'db/migrate/20171125031751_add_invite_id_to_users.rb' + - 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb' + - 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb' + - 'db/migrate/20171129172043_add_index_on_stream_entries.rb' + - 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb' + - 'db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb' + - 'db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb' + - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb' + - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20180109143959_add_remember_token_to_users.rb' + - 'db/migrate/20180204034416_create_identities.rb' + - 'db/migrate/20180206000000_change_user_id_nonnullable.rb' + - 'db/migrate/20180211015820_create_backups.rb' + - 'db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb' + - 'db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb' + - 'db/migrate/20180402031200_add_assigned_account_id_to_reports.rb' + - 'db/migrate/20180402040909_create_report_notes.rb' + - 'db/migrate/20180410204633_add_fields_to_accounts.rb' + - 'db/migrate/20180416210259_add_uri_to_relationships.rb' + - 'db/migrate/20180506221944_add_actor_type_to_accounts.rb' + - 'db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb' + - 'db/migrate/20180510230049_migrate_web_push_subscriptions.rb' + - 'db/migrate/20180528141303_fix_accounts_unique_index.rb' + - 'db/migrate/20180608213548_reject_following_blocked_users.rb' + - 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb' + - 'db/migrate/20180615122121_add_autofollow_to_invites.rb' + - 'db/migrate/20180616192031_add_chosen_languages_to_users.rb' + - 'db/migrate/20180617162849_remove_unused_indexes.rb' + - 'db/migrate/20180628181026_create_custom_filters.rb' + - 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb' + - 'db/migrate/20180711152640_create_relays.rb' + - 'db/migrate/20180808175627_create_account_pins.rb' + - 'db/migrate/20180812123222_change_relays_enabled.rb' + - 'db/migrate/20180812162710_create_status_stats.rb' + - 'db/migrate/20180812173710_copy_status_stats.rb' + - 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb' + - 'db/migrate/20180831171112_create_bookmarks.rb' + - 'db/migrate/20180929222014_create_account_conversations.rb' + - 'db/migrate/20181007025445_create_pghero_space_stats.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/20181024224956_migrate_account_conversations.rb' + - 'db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb' + - 'db/migrate/20181116165755_create_account_stats.rb' + - 'db/migrate/20181116173541_copy_account_stats.rb' + - 'db/migrate/20181127130500_identity_id_to_bigint.rb' + - 'db/migrate/20181127165847_add_show_replies_to_lists.rb' + - 'db/migrate/20181203003808_create_accounts_tags_join_table.rb' + - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb' + - 'db/migrate/20181204193439_add_last_status_at_to_account_stats.rb' + - 'db/migrate/20181204215309_create_account_tag_stats.rb' + - 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb' + - 'db/migrate/20181213184704_create_account_warnings.rb' + - 'db/migrate/20181213185533_create_account_warning_presets.rb' + - 'db/migrate/20181219235220_add_created_by_application_id_to_users.rb' + - 'db/migrate/20181226021420_add_also_known_as_to_accounts.rb' + - 'db/migrate/20190103124649_create_scheduled_statuses.rb' + - 'db/migrate/20190103124754_add_scheduled_status_id_to_media_attachments.rb' + - 'db/migrate/20190117114553_create_tombstones.rb' + - 'db/migrate/20190201012802_add_overwrite_to_imports.rb' + - 'db/migrate/20190203180359_create_featured_tags.rb' + - 'db/migrate/20190225031541_create_polls.rb' + - 'db/migrate/20190225031625_create_poll_votes.rb' + - 'db/migrate/20190226003449_add_poll_id_to_statuses.rb' + - 'db/migrate/20190304152020_add_uri_to_poll_votes.rb' + - 'db/migrate/20190306145741_add_lock_version_to_polls.rb' + - 'db/migrate/20190307234537_add_approved_to_users.rb' + - 'db/migrate/20190314181829_migrate_open_registrations_setting.rb' + - 'db/migrate/20190316190352_create_account_identity_proofs.rb' + - 'db/migrate/20190317135723_add_uri_to_reports.rb' + - 'db/migrate/20190403141604_add_comment_to_invites.rb' + - 'db/migrate/20190409054914_create_user_invite_requests.rb' + - 'db/migrate/20190420025523_add_blurhash_to_media_attachments.rb' + - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' + - 'db/migrate/20190627222225_create_custom_emoji_categories.rb' + - 'db/migrate/20190627222826_add_category_id_to_custom_emojis.rb' + - 'db/migrate/20190701022101_add_trust_level_to_accounts.rb' + - 'db/migrate/20190705002136_create_domain_allows.rb' + - 'db/migrate/20190715164535_add_instance_actor.rb' + - 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb' + - 'db/migrate/20190729185330_add_score_to_tags.rb' + - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' + - 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb' + - 'db/migrate/20190815225426_add_last_status_at_to_tags.rb' + - 'db/migrate/20190819134503_add_deleted_at_to_statuses.rb' + - 'db/migrate/20190820003045_update_statuses_index.rb' + - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' + - 'db/migrate/20190901035623_add_max_score_to_tags.rb' + - 'db/migrate/20190904222339_create_markers.rb' + - 'db/migrate/20190914202517_create_account_migrations.rb' + - 'db/migrate/20190915194355_create_account_aliases.rb' + - 'db/migrate/20190927232842_add_voters_count_to_polls.rb' + - 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb' + - 'db/migrate/20191007013357_update_pt_locales.rb' + - 'db/migrate/20191031163205_change_list_account_follow_nullable.rb' + - 'db/migrate/20191212003415_increase_backup_size.rb' + - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb' + - 'db/migrate/20191218153258_create_announcements.rb' + - 'db/migrate/20200113125135_create_announcement_mutes.rb' + - 'db/migrate/20200114113335_create_announcement_reactions.rb' + - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' + - 'db/migrate/20200126203551_add_published_at_to_announcements.rb' + - 'db/migrate/20200306035625_add_processing_to_media_attachments.rb' + - 'db/migrate/20200309150742_add_forwarded_to_reports.rb' + - 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb' + - 'db/migrate/20200312162302_add_status_ids_to_announcements.rb' + - 'db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb' + - 'db/migrate/20200317021758_add_expires_at_to_mutes.rb' + - 'db/migrate/20200407201300_create_unavailable_domains.rb' + - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb' + - 'db/migrate/20200417125749_add_storage_schema_version.rb' + - 'db/migrate/20200508212852_reset_unique_jobs_locks.rb' + - 'db/migrate/20200510110808_reset_web_app_secret.rb' + - 'db/migrate/20200510181721_remove_duplicated_indexes_pghero.rb' + - 'db/migrate/20200516180352_create_devices.rb' + - 'db/migrate/20200516183822_create_one_time_keys.rb' + - 'db/migrate/20200518083523_create_encrypted_messages.rb' + - 'db/migrate/20200521180606_encrypted_message_ids_to_timestamp_ids.rb' + - 'db/migrate/20200529214050_add_devices_url_to_accounts.rb' + - 'db/migrate/20200601222558_create_system_keys.rb' + - 'db/migrate/20200605155027_add_blurhash_to_preview_cards.rb' + - 'db/migrate/20200608113046_add_sign_in_token_to_users.rb' + - 'db/migrate/20200614002136_add_sensitized_to_accounts.rb' + - 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb' + - 'db/migrate/20200622213645_media_attachment_ids_to_timestamp_ids.rb' + - 'db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb' + - 'db/migrate/20200628133322_create_account_notes.rb' + - 'db/migrate/20200630190240_create_webauthn_credentials.rb' + - 'db/migrate/20200630190544_add_webauthn_id_to_users.rb' + - 'db/migrate/20200908193330_create_account_deletion_requests.rb' + - 'db/migrate/20200917192924_add_notify_to_follows.rb' + - 'db/migrate/20200917193034_add_type_to_notifications.rb' + - 'db/migrate/20200917222316_add_index_notifications_on_type.rb' + - 'db/migrate/20201008202037_create_ip_blocks.rb' + - 'db/migrate/20201008220312_add_sign_up_ip_to_users.rb' + - 'db/migrate/20201017233919_add_suspension_origin_to_accounts.rb' + - 'db/migrate/20201206004238_create_instances.rb' + - 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb' + - 'db/migrate/20210221045109_create_rules.rb' + - 'db/migrate/20210306164523_account_ids_to_timestamp_ids.rb' + - 'db/migrate/20210322164601_create_account_summaries.rb' + - 'db/migrate/20210323114347_create_follow_recommendations.rb' + - 'db/migrate/20210324171613_create_follow_recommendation_suppressions.rb' + - 'db/migrate/20210416200740_create_canonical_email_blocks.rb' + - 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb' + - 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb' + - 'db/migrate/20210505174616_update_follow_recommendations_to_version_2.rb' + - 'db/migrate/20210609202149_create_login_activities.rb' + - 'db/migrate/20210616214526_create_user_ips.rb' + - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb' + - 'db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb' + - 'db/migrate/20210722120340_create_account_statuses_cleanup_policies.rb' + - 'db/migrate/20210904215403_add_edited_at_to_statuses.rb' + - 'db/migrate/20210908220918_create_status_edits.rb' + - 'db/migrate/20211031031021_create_preview_card_providers.rb' + - 'db/migrate/20211112011713_add_language_to_preview_cards.rb' + - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb' + - 'db/migrate/20211123212714_add_link_type_to_preview_cards.rb' + - 'db/migrate/20211213040746_update_account_summaries_to_version_2.rb' + - 'db/migrate/20211231080958_add_category_to_reports.rb' + - 'db/migrate/20220105163928_remove_mentions_status_id_index.rb' + - 'db/migrate/20220115125126_add_report_id_to_account_warnings.rb' + - 'db/migrate/20220115125341_fix_account_warning_actions.rb' + - 'db/migrate/20220116202951_add_deleted_at_index_on_statuses.rb' + - 'db/migrate/20220124141035_create_appeals.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' + - 'db/migrate/20220202200926_add_trendable_to_statuses.rb' + - 'db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb' + - 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb' + - 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb' + - 'db/migrate/20220302232632_add_ordered_media_attachment_ids_to_statuses.rb' + - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' + - 'db/migrate/20220304195405_migrate_hide_network_preference.rb' + - 'db/migrate/20220307094650_fix_featured_tags_constraints.rb' + - 'db/migrate/20220309213005_fix_reblog_deleted_at.rb' + - 'db/migrate/20220316233212_update_kurdish_locales.rb' + - 'db/migrate/20220428112511_add_index_statuses_on_account_id.rb' + - 'db/migrate/20220428112727_add_index_statuses_pins_on_status_id.rb' + - 'db/migrate/20220428114454_add_index_reports_on_assigned_account_id.rb' + - 'db/migrate/20220428114902_add_index_reports_on_action_taken_by_account_id.rb' + - 'db/migrate/20220606044941_create_webhooks.rb' + - 'db/migrate/20220611210335_create_user_roles.rb' + - 'db/migrate/20220611212541_add_role_id_to_users.rb' + - 'db/migrate/20220710102457_add_display_name_to_tags.rb' + - 'db/migrate/20220714171049_create_tag_follows.rb' + - 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb' + - 'db/migrate/20220824233535_create_status_trends.rb' + - 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb' + - 'db/migrate/20220829192633_add_languages_to_follows.rb' + - 'db/migrate/20220829192658_add_languages_to_follow_requests.rb' + - 'db/migrate/20221006061337_create_preview_card_trends.rb' + - 'db/migrate/20221012181003_add_blurhash_to_site_uploads.rb' + - 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb' + - 'db/migrate/20221025171544_add_index_ip_blocks_on_ip.rb' + - 'db/migrate/20221104133904_add_name_to_featured_tags.rb' + - 'db/post_migrate/20190519130537_remove_boosts_widening_audience.rb' + - 'db/post_migrate/20210308133107_remove_subscription_expires_at_from_accounts.rb' + - 'db/post_migrate/20220118183123_remove_rememberable_from_users.rb' + - 'db/seeds/01_web_app.rb' + - 'db/seeds/02_instance_actor.rb' + - 'db/seeds/03_roles.rb' + - 'db/seeds/04_admin.rb' + - 'lib/rails/engine_extensions.rb' + - 'lib/tasks/branding.rake' + - 'spec/fabricators_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/GlobalStdStream: + Exclude: + - 'config/boot.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + +# Configuration parameters: AllowedVariables. +Style/GlobalVars: + Exclude: + - 'config/initializers/statsd.rb' -# Offense count: 34 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: @@ -2013,7 +1425,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' @@ -2030,6 +1441,7 @@ Style/GuardClause: - 'app/workers/redownload_header_worker.rb' - 'app/workers/redownload_media_worker.rb' - 'app/workers/remote_account_refresh_worker.rb' + - 'config/initializers/devise.rb' - 'db/migrate/20170901141119_truncate_preview_cards.rb' - 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb' - 'lib/devise/two_factor_ldap_authenticatable.rb' @@ -2040,7 +1452,6 @@ Style/GuardClause: - 'lib/paperclip/attachment_extensions.rb' - 'lib/tasks/repo.rake' -# Offense count: 13 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: braces, no_braces @@ -2048,7 +1459,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' @@ -2056,35 +1466,19 @@ Style/HashAsLastArrayItem: - 'app/services/notify_service.rb' - 'db/migrate/20181024224956_migrate_account_conversations.rb' -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowSplatArgument. -Style/HashConversion: - Exclude: - - 'app/services/import_service.rb' - -# Offense count: 12 -# 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' - -# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). Style/HashTransformValues: Exclude: - 'app/serializers/rest/web_push_subscription_serializer.rb' - 'app/services/import_service.rb' -# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/ffmpeg.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: InverseMethods, InverseBlocks. Style/InverseMethods: @@ -2093,36 +1487,41 @@ Style/InverseMethods: - 'app/services/update_account_service.rb' - 'spec/controllers/activitypub/replies_controller_spec.rb' -# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: line_count_dependent, lambda, literal +Style/Lambda: + Exclude: + - 'config/initializers/simple_form.rb' + - 'config/routes.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). Style/MapToHash: Exclude: - 'app/models/status.rb' -# Offense count: 17 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # 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' - - 'lib/mastodon/snowflake.rb' - - 'spec/controllers/api/base_controller_spec.rb' + - 'config/initializers/twitter_regex.rb' + - 'lib/mastodon/migration_warning.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/NilLambda: + Exclude: + - 'config/initializers/paperclip.rb' -# Offense count: 10 # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - 'app/helpers/admin/account_moderation_notes_helper.rb' - 'app/helpers/jsonld_helper.rb' + - 'app/lib/admin/system_check/message.rb' - 'app/lib/request.rb' - 'app/lib/webfinger.rb' - 'app/services/block_domain_service.rb' @@ -2131,24 +1530,39 @@ Style/OptionalBooleanParameter: - 'app/workers/unfollow_follow_worker.rb' - 'lib/mastodon/redis_config.rb' -# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'config/deploy.rb' + - 'config/initializers/doorkeeper.rb' + # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: short, verbose Style/PreferredHashMethods: Exclude: - - 'spec/support/matchers/model/model_have_error_on_field.rb' + - 'config/initializers/paperclip.rb' -# Offense count: 5 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Methods. -Style/RedundantArgument: +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantConstantBase: Exclude: - - 'app/controllers/concerns/signature_verification.rb' - - 'app/helpers/application_helper.rb' - - 'lib/tasks/emojis.rake' + - 'config/environments/production.rb' + - 'config/initializers/sidekiq.rb' + - 'config/initializers/statsd.rb' + - 'config/locales/sr-Latn.rb' + - 'config/locales/sr.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SafeForConstants. +Style/RedundantFetchBlock: + Exclude: + - 'config/initializers/1_hosts.rb' + - 'config/initializers/chewy.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/paperclip.rb' + - 'config/puma.rb' -# Offense count: 16 # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpCharacterClass: Exclude: @@ -2157,10 +1571,10 @@ Style/RedundantRegexpCharacterClass: - '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' -# Offense count: 10 # This cop supports safe autocorrection (--autocorrect). Style/RedundantRegexpEscape: Exclude: @@ -2168,17 +1582,16 @@ Style/RedundantRegexpEscape: - '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' -# Offense count: 19 # 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' @@ -2190,10 +1603,12 @@ Style/RegexpLiteral: - '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' -# Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! @@ -2202,16 +1617,13 @@ Style/SafeNavigation: - 'app/models/concerns/account_finder_concern.rb' - 'app/models/status.rb' -# Offense count: 5 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowAsExpressionSeparator. 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' -# Offense count: 2 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: only_raise, only_fail, semantic @@ -2220,19 +1632,16 @@ Style/SignalException: - 'lib/devise/two_factor_ldap_authenticatable.rb' - 'lib/devise/two_factor_pam_authenticatable.rb' -# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). Style/SingleArgumentDig: Exclude: - 'lib/webpacker/manifest_extensions.rb' -# Offense count: 14 # This cop supports unsafe autocorrection (--autocorrect-all). Style/SlicingWithRange: Exclude: - 'app/lib/emoji_formatter.rb' - 'app/lib/text_formatter.rb' - - 'app/lib/toc_generator.rb' - 'app/models/account_alias.rb' - 'app/models/domain_block.rb' - 'app/models/email_domain_block.rb' @@ -2243,133 +1652,78 @@ Style/SlicingWithRange: - 'lib/mastodon/premailer_webpack_strategy.rb' - 'lib/tasks/repo.rake' -# Offense count: 272 # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MinSize. -# SupportedStyles: percent, brackets -Style/SymbolArray: +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_parentheses, require_no_parentheses +Style/StabbyLambdaParentheses: Exclude: - - 'app/controllers/accounts_controller.rb' - - 'app/controllers/activitypub/replies_controller.rb' - - 'app/controllers/admin/accounts_controller.rb' - - 'app/controllers/admin/announcements_controller.rb' - - 'app/controllers/admin/domain_blocks_controller.rb' - - 'app/controllers/admin/email_domain_blocks_controller.rb' - - 'app/controllers/admin/relationships_controller.rb' - - 'app/controllers/admin/relays_controller.rb' - - 'app/controllers/admin/roles_controller.rb' - - 'app/controllers/admin/rules_controller.rb' - - 'app/controllers/admin/statuses_controller.rb' - - 'app/controllers/admin/trends/statuses_controller.rb' - - 'app/controllers/admin/warning_presets_controller.rb' - - 'app/controllers/admin/webhooks_controller.rb' - - 'app/controllers/api/v1/accounts/credentials_controller.rb' - - 'app/controllers/api/v1/accounts_controller.rb' - - 'app/controllers/api/v1/admin/accounts_controller.rb' - - 'app/controllers/api/v1/admin/canonical_email_blocks_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/admin/reports_controller.rb' - - 'app/controllers/api/v1/crypto/deliveries_controller.rb' - - 'app/controllers/api/v1/crypto/keys/claims_controller.rb' - - 'app/controllers/api/v1/crypto/keys/uploads_controller.rb' - - 'app/controllers/api/v1/featured_tags_controller.rb' - - 'app/controllers/api/v1/filters_controller.rb' - - 'app/controllers/api/v1/lists_controller.rb' - - 'app/controllers/api/v1/notifications_controller.rb' - - 'app/controllers/api/v1/push/subscriptions_controller.rb' - - 'app/controllers/api/v1/scheduled_statuses_controller.rb' - - 'app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb' - - 'app/controllers/api/v1/statuses_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/api/web/push_subscriptions_controller.rb' - - 'app/controllers/application_controller.rb' - - 'app/controllers/auth/registrations_controller.rb' - - 'app/controllers/filters_controller.rb' - - 'app/controllers/settings/applications_controller.rb' - - 'app/controllers/settings/featured_tags_controller.rb' - - 'app/controllers/settings/profiles_controller.rb' - - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' - - 'app/controllers/statuses_controller.rb' - - 'app/lib/feed_manager.rb' - - 'app/models/account.rb' - - 'app/models/account_filter.rb' - - 'app/models/admin/status_filter.rb' - - 'app/models/announcement.rb' - - 'app/models/concerns/ldap_authenticable.rb' - - 'app/models/concerns/status_threading_concern.rb' - - 'app/models/custom_filter.rb' - - 'app/models/domain_block.rb' - - 'app/models/import.rb' - - 'app/models/list.rb' - - 'app/models/media_attachment.rb' - - 'app/models/preview_card.rb' - - 'app/models/relay.rb' - - 'app/models/report.rb' - - 'app/models/site_upload.rb' - - 'app/models/status.rb' - - 'app/serializers/initial_state_serializer.rb' - - 'app/serializers/rest/notification_serializer.rb' - - 'db/migrate/20160220174730_create_accounts.rb' - - 'db/migrate/20160221003621_create_follows.rb' - - 'db/migrate/20160223171800_create_favourites.rb' - - 'db/migrate/20160224223247_create_mentions.rb' - - 'db/migrate/20160314164231_add_owner_to_application.rb' - - 'db/migrate/20160316103650_add_missing_indices.rb' - - 'db/migrate/20160926213048_remove_owner_from_application.rb' - - 'db/migrate/20161003145426_create_blocks.rb' - - 'db/migrate/20161006213403_rails_settings_migration.rb' - - 'db/migrate/20161105130633_create_statuses_tags_join_table.rb' - - 'db/migrate/20161119211120_create_notifications.rb' - - 'db/migrate/20161128103007_create_subscriptions.rb' - - 'db/migrate/20161222204147_create_follow_requests.rb' - - 'db/migrate/20170112154826_migrate_settings.rb' - - 'db/migrate/20170301222600_create_mutes.rb' - - 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb' - - 'db/migrate/20170424003227_create_account_domain_blocks.rb' - - 'db/migrate/20170427011934_re_add_owner_to_application.rb' - - 'db/migrate/20170507141759_optimize_index_subscriptions.rb' - - 'db/migrate/20170508230434_create_conversation_mutes.rb' - - 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb' - - 'db/migrate/20170823162448_create_status_pins.rb' - - 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb' - - 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb' - - 'db/migrate/20170917153509_create_custom_emojis.rb' - - 'db/migrate/20170918125918_ids_to_bigints.rb' - - 'db/migrate/20171116161857_create_list_accounts.rb' - - 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb' - - 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb' - - 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb' - - 'db/migrate/20171129172043_add_index_on_stream_entries.rb' - - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb' - - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' - - 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb' - - 'db/migrate/20180808175627_create_account_pins.rb' - - 'db/migrate/20180831171112_create_bookmarks.rb' - - 'db/migrate/20180929222014_create_account_conversations.rb' - - 'db/migrate/20181007025445_create_pghero_space_stats.rb' - - 'db/migrate/20181203003808_create_accounts_tags_join_table.rb' - - 'db/migrate/20190316190352_create_account_identity_proofs.rb' - - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - - 'db/migrate/20190820003045_update_statuses_index.rb' - - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' - - 'db/migrate/20190904222339_create_markers.rb' - - 'db/migrate/20200113125135_create_announcement_mutes.rb' - - 'db/migrate/20200114113335_create_announcement_reactions.rb' - - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' - - 'db/migrate/20200628133322_create_account_notes.rb' - - 'db/migrate/20200917222316_add_index_notifications_on_type.rb' - - 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb' - - 'db/migrate/20220714171049_create_tag_follows.rb' - - 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb' - - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' - - 'db/post_migrate/20200917222734_remove_index_notifications_on_account_activity.rb' - - 'spec/controllers/api/v1/streaming_controller_spec.rb' - - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/fabricators/notification_fabricator.rb' - - 'spec/models/public_feed_spec.rb' + - 'config/environments/production.rb' + - 'config/initializers/content_security_policy.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/StderrPuts: + Exclude: + - 'config/boot.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/backtrace_silencers.rb' + - 'config/initializers/http_client_proxy.rb' + - 'config/initializers/rack_attack.rb' + - 'config/initializers/webauthn.rb' + - 'config/routes.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. +# AllowedMethods: define_method, mail, respond_to +Style/SymbolProc: + Exclude: + - 'config/initializers/omniauth.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, AllowSafeAssignment. +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex +Style/TernaryParentheses: + Exclude: + - 'config/environments/development.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInArguments: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInHashLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/environments/test.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MinSize, WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + 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. +# URISchemes: http, https +Layout/LineLength: + Max: 701 diff --git a/.ruby-version b/.ruby-version index e4604e3af..be94e6f53 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.1 +3.2.2 diff --git a/.yarnclean b/.yarnclean index 0cc2b50d7..21eb734a6 100644 --- a/.yarnclean +++ b/.yarnclean @@ -44,3 +44,6 @@ Gruntfile.js # for specific ignore !.svgo.yml !sass-lint/**/*.yml + +# breaks lint-staged or generally anything using https://github.com/eemeli/yaml/issues/384 +!**/yaml/dist/**/doc 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 2b826fb14..91a2c48a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,71 @@ All notable changes to this project will be documented in this file. +## [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 + +- Add redirection from paths with url-encoded `@` to their decoded form ([thijskh](https://github.com/mastodon/mastodon/pull/23593)) +- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749)) +- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597)) +- Add support for refreshing many accounts at once with `tootctl accounts refresh` ([9p4](https://github.com/mastodon/mastodon/pull/23304)) +- Add confirmation modal when clicking to edit a post with a non-empty compose form ([PauloVilarinho](https://github.com/mastodon/mastodon/pull/23936)) +- Add support for the HAproxy PROXY protocol through the `PROXY_PROTO_V1` environment variable ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24064)) +- Add `SENDFILE_HEADER` environment variable ([Gargron](https://github.com/mastodon/mastodon/pull/24123)) +- Add cache headers to static files served through Rails ([Gargron](https://github.com/mastodon/mastodon/pull/24120)) + +### Changed + +- Increase contrast of upload progress bar background ([toolmantim](https://github.com/mastodon/mastodon/pull/23836)) +- Change post auto-deletion throttling constants to better scale with server size ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23320)) +- Change order of bookmark and favourite sidebar entries in single-column UI for consistency ([TerryGarcia](https://github.com/mastodon/mastodon/pull/23701)) +- Change `ActivityPub::DeliveryWorker` retries to be spread out more ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21956)) + +### Fixed + +- Fix “Remove all followers from the selected domains” also removing follows and notifications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805)) +- Fix streaming metrics format ([emilweth](https://github.com/mastodon/mastodon/pull/23519), [emilweth](https://github.com/mastodon/mastodon/pull/23520)) +- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526)) +- Fix focus point of already-attached media not saving after edit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23566)) +- Fix sidebar behavior in settings/admin UI on mobile ([wxt2005](https://github.com/mastodon/mastodon/pull/23764)) +- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801)) +- Fix duplicate “Publish” button on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23804)) +- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787)) +- Fix server error when attempting to display the edit history of a trendable post in the admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23574)) +- Fix `tootctl accounts migrate` crashing because of a typo ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23567)) +- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957)) +- Fix the “Back” button in column headers sometimes leaving Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953)) +- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958)) +- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803)) +- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988)) +- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029)) +- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046)) +- Fix tags being unnecessarily stripped from plain-text short site description ([c960657](https://github.com/mastodon/mastodon/pull/23975)) +- Fix HTML entities not being un-escaped in extracted plain-text from remote posts ([c960657](https://github.com/mastodon/mastodon/pull/24019)) +- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751)) +- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611)) +- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568)) +- Fix duplicate mails being sent when the SMTP server is too slow to close the connection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23750)) + +### Security + +- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136)) +- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137)) + ## [4.1.0] - 2023-02-10 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f09f40006..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. @@ -80,8 +80,6 @@ It is not always possible to phrase every change in such a manner, but it is des - Code style rules (rubocop, eslint) - Normalization of locale files (i18n-tasks) -**Note**: You may need to log in and authorise the GitHub account your fork of this repository belongs to with CircleCI to enable some of the automated checks to run. - ## Documentation The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). diff --git a/Dockerfile b/Dockerfile index c2b18ce88..cb5096581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # 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.1-slim as ruby +FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby FROM node:${NODE_VERSION} as build COPY --link --from=ruby /opt/ruby /opt/ruby @@ -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 \ @@ -37,11 +36,15 @@ RUN apt-get update && \ bundle config set --local without 'development test' && \ bundle config set silence_root_warning true && \ bundle install -j"$(nproc)" && \ - yarn install --pure-lockfile --network-timeout 600000 && \ + yarn install --pure-lockfile --production --network-timeout 600000 && \ yarn cache clean 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 42340f58d..88ac7c5ae 100644 --- a/Gemfile +++ b/Gemfile @@ -1,24 +1,23 @@ # 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 'rexml', '~> 3.2' -gem 'puma', '~> 5.6' +gem 'puma', '~> 6.2' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' -gem 'rack', '~> 2.2.6' +gem 'rack', '~> 2.2.7' gem 'haml-rails', '~>2.0' -gem 'pg', '~> 1.4' +gem 'pg', '~> 1.5' gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.119', require: false +gem 'aws-sdk-s3', '~> 1.122', 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' @@ -29,18 +28,18 @@ 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.0' +gem 'omniauth_openid_connect', '~> 0.6.1' gem 'omniauth', '~> 1.9' gem 'omniauth-rails_csrf_protection', '~> 0.1' @@ -70,14 +69,14 @@ gem 'public_suffix', '~> 5.0' gem 'pundit', '~> 2.3' gem 'premailer-rails' gem 'rack-attack', '~> 6.6' -gem 'rack-cors', '~> 1.1', require: 'rack/cors' +gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rails-i18n', '~> 6.0' gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' 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' @@ -88,10 +87,10 @@ gem 'simple-navigation', '~> 4.4' gem 'simple_form', '~> 5.2' gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' gem 'stoplight', '~> 3.0.1' -gem 'strong_migrations', '~> 0.7' +gem 'strong_migrations', '~> 0.8' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' -gem 'tzinfo-data', '~> 1.2022' +gem 'tzinfo-data', '~> 1.2023' gem 'webpacker', '~> 5.4' gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' gem 'webauthn', '~> 3.0' @@ -100,54 +99,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 'pry-byebug', '~> 3.10' - gem 'pry-rails', '~> 0.3' - gem 'rspec-rails', '~> 5.1' - 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', '~> 0.2' - gem 'faker', '~> 3.1' - gem 'json-schema', '~> 3.0' - gem 'rack-test', '~> 2.0' - 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 - gem 'active_record_query_trace', '~> 1.8' + # 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' - gem 'bullet', '~> 7.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 @@ -158,7 +190,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 0deb4542d..21647e1b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,40 +30,40 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.2) - actionpack (= 6.1.7.2) - activesupport (= 6.1.7.2) + actioncable (6.1.7.3) + actionpack (= 6.1.7.3) + activesupport (= 6.1.7.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.2) - actionpack (= 6.1.7.2) - activejob (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + 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) mail (>= 2.7.1) - actionmailer (6.1.7.2) - actionpack (= 6.1.7.2) - actionview (= 6.1.7.2) - activejob (= 6.1.7.2) - activesupport (= 6.1.7.2) + 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) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.2) - actionview (= 6.1.7.2) - activesupport (= 6.1.7.2) + actionpack (6.1.7.3) + actionview (= 6.1.7.3) + activesupport (= 6.1.7.3) 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.2) - actionpack (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + 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) nokogiri (>= 1.8.5) - actionview (6.1.7.2) - activesupport (= 6.1.7.2) + actionview (6.1.7.3) + activesupport (= 6.1.7.3) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -73,29 +73,28 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - active_record_query_trace (1.8) - activejob (6.1.7.2) - activesupport (= 6.1.7.2) + activejob (6.1.7.3) + activesupport (= 6.1.7.3) globalid (>= 0.3.6) - activemodel (6.1.7.2) - activesupport (= 6.1.7.2) - activerecord (6.1.7.2) - activemodel (= 6.1.7.2) - activesupport (= 6.1.7.2) - activestorage (6.1.7.2) - actionpack (= 6.1.7.2) - activejob (= 6.1.7.2) - activerecord (= 6.1.7.2) - activesupport (= 6.1.7.2) + 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) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.2) + activesupport (6.1.7.3) 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) @@ -105,21 +104,21 @@ 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.711.0) - aws-sdk-core (3.170.0) + aws-partitions (1.761.0) + aws-sdk-core (3.172.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.62.0) + aws-sdk-kms (1.64.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.119.1) + aws-sdk-s3 (1.122.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -143,25 +142,21 @@ GEM blurhash (0.1.7) bootsnap (1.16.0) msgpack (~> 1.2) - brakeman (5.4.0) - browser (4.2.0) + brakeman (5.4.1) + browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) builder (3.2.4) - bullet (7.0.7) - activesupport (>= 3.0.0) - uniform_notifier (~> 1.11) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - byebug (11.1.3) capistrano (3.17.2) 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) @@ -171,7 +166,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.38.0) + capybara (3.39.1) addressable matrix mini_mime (>= 0.1.3) @@ -184,7 +179,7 @@ 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 @@ -193,27 +188,27 @@ GEM cocoon (1.2.15) coderay (1.1.3) color_diff (0.1) - concurrent-ruby (1.2.0) - connection_pool (2.3.0) + concurrent-ruby (1.2.2) + 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) - devise (4.9.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) @@ -226,7 +221,7 @@ GEM docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.6.4) + doorkeeper (5.6.6) railties (>= 5) dotenv (2.8.1) dotenv-rails (2.8.1) @@ -246,9 +241,9 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.95.0) + excon (0.99.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) @@ -309,11 +304,17 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) + haml_lint (0.45.0) + haml (>= 4.0, < 6.2) + parallel (~> 1.10) + rainbow + rubocop (>= 0.50.0) + sysexits (~> 1.1) hashdiff (1.0.1) 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) @@ -330,7 +331,7 @@ GEM httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.12.0) + i18n (1.13.0) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -347,23 +348,23 @@ 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) @@ -379,8 +380,8 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - launchy (2.5.0) - addressable (~> 2.7) + launchy (2.5.2) + addressable (~> 2.8) letter_opener (1.8.1) launchy (>= 2.2, < 3) letter_opener_web (2.0.0) @@ -397,10 +398,10 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.19.1) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.8.0.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) net-imap net-pop @@ -415,11 +416,11 @@ GEM method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.0218.1) mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.17.0) - msgpack (1.6.0) + mini_portile2 (2.8.2) + minitest (5.18.0) + msgpack (1.7.0) multi_json (1.15.0) multipart-post (2.3.0) net-http (0.3.2) @@ -427,7 +428,7 @@ GEM net-imap (0.3.4) 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) @@ -436,9 +437,9 @@ 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) + net-ssh (7.1.0) + nio4r (2.5.9) + nokogiri (1.14.3) mini_portile2 (~> 2.8.0) racc (~> 1.4) nsa (0.2.8) @@ -446,7 +447,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.14.2) + oj (3.14.3) omniauth (1.9.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -460,7 +461,7 @@ GEM omniauth-saml (1.10.3) omniauth (~> 1.3, >= 1.3.2) ruby-saml (~> 1.9) - omniauth_openid_connect (0.6.0) + omniauth_openid_connect (0.6.1) omniauth (>= 1.9, < 3) openid_connect (~> 1.1) openid_connect (1.4.2) @@ -478,19 +479,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.0) + ox (2.14.16) + parallel (1.23.0) + parser (3.2.2.1) ast (~> 2.4.1) parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.4.5) - pghero (3.1.0) + 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) @@ -499,25 +500,17 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) - pry (0.14.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) - pry-rails (0.3.9) - pry (>= 0.10.4) public_suffix (5.0.1) - puma (5.6.5) + puma (6.2.2) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.6.2) - rack (2.2.6.2) + rack (2.2.7) rack-attack (6.6.1) rack (>= 1.0, < 3) - rack-cors (1.1.1) + rack-cors (2.0.1) rack (>= 2.0.0) rack-oauth2 (1.21.3) activesupport @@ -527,22 +520,22 @@ GEM rack (>= 2.1.0) rack-proxy (0.7.6) rack - rack-test (2.0.2) + rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.2) - actioncable (= 6.1.7.2) - actionmailbox (= 6.1.7.2) - actionmailer (= 6.1.7.2) - actionpack (= 6.1.7.2) - actiontext (= 6.1.7.2) - actionview (= 6.1.7.2) - activejob (= 6.1.7.2) - activemodel (= 6.1.7.2) - activerecord (= 6.1.7.2) - activestorage (= 6.1.7.2) - activesupport (= 6.1.7.2) + 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) bundler (>= 1.15.0) - railties (= 6.1.7.2) + railties (= 6.1.7.3) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -556,88 +549,88 @@ GEM rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) - railties (6.1.7.2) - actionpack (= 6.1.7.2) - activesupport (= 6.1.7.2) + railties (6.1.7.3) + actionpack (= 6.1.7.3) + activesupport (= 6.1.7.3) method_source rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) rake (13.0.6) - rdf (3.2.9) + rdf (3.2.10) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.5.1) rdf (~> 3.2) redcarpet (3.6.0) - redis (4.5.1) + redis (4.8.1) redis-namespace (1.10.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.7.0) + regexp_parser (2.8.0) request_store (1.5.1) rack (>= 1.4) responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) rexml (3.2.5) - rotp (6.2.0) + rotp (6.2.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.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-support (~> 3.12.0) + rspec-rails (6.0.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + 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.11.1) - rspec_junit_formatter (0.6.0) - rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.45.1) + rspec-support (3.12.0) + rspec_chunked (0.6) + rubocop (1.51.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.24.1, < 2.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.24.1) - parser (>= 3.1.1.0) - rubocop-capybara (2.17.0) + rubocop-ast (1.28.1) + parser (>= 3.2.1.0) + rubocop-capybara (2.18.0) rubocop (~> 1.41) - rubocop-performance (1.16.0) + rubocop-performance (1.18.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.17.4) + rubocop-rails (2.19.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.18.1) + rubocop-rspec (2.19.0) rubocop (~> 1.33) rubocop-capybara (~> 2.17) - ruby-progressbar (1.11.0) + ruby-progressbar (1.13.0) ruby-saml (1.13.0) nokogiri (>= 1.10.5) rexml ruby2_keywords (0.0.5) + rubyzip (2.3.2) rufus-scheduler (3.8.2) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) @@ -655,9 +648,9 @@ GEM redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (5.0.1) + sidekiq-scheduler (5.0.2) rufus-scheduler (~> 3.2) - sidekiq (>= 4, < 8) + sidekiq (>= 6, < 8) tilt (>= 1.4.0) sidekiq-unique-jobs (7.1.29) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) @@ -687,24 +680,25 @@ GEM sshkit (1.21.4) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.23) + stackprof (0.2.25) statsd-ruby (1.5.0) stoplight (3.0.1) redlock (~> 1.0) - strong_migrations (0.7.9) - activerecord (>= 5) + strong_migrations (0.8.0) + activerecord (>= 5.2) swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) httpclient (>= 2.4) + sysexits (1.2.0) temple (0.10.0) 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.0.11) - timeout (0.3.1) + thor (1.2.2) + tilt (2.1.0) + timeout (0.3.2) tpm-key_attestation (0.12.0) bindata (~> 2.4) openssl (> 2.0) @@ -724,14 +718,13 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2022.7) + tzinfo-data (1.2023.3) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - uniform_notifier (1.16.0) - uri (0.12.0) + uri (0.12.1) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -768,52 +761,51 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.7) + zeitwerk (2.6.8) PLATFORMS ruby DEPENDENCIES active_model_serializers (~> 0.10) - active_record_query_trace (~> 1.8) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.119) + aws-sdk-s3 (~> 1.122) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) bootsnap (~> 1.16.0) brakeman (~> 5.4) browser - bullet (~> 7.0) bundler-audit (~> 0.9) capistrano (~> 3.17) 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) + 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) fog-openstack (~> 0.3) fuubar (~> 2.5) haml-rails (~> 2.0) + haml_lint hcaptcha (~> 7.1) hiredis (~> 0.6) htmlentities (~> 4.3) @@ -824,7 +816,7 @@ DEPENDENCIES idn-ruby json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 3.0) + json-schema (~> 4.0) kaminari (~> 1.2) kt-paperclip (~> 7.1)! letter_opener (~> 1.8) @@ -836,7 +828,7 @@ DEPENDENCIES memory_profiler mime-types (~> 3.4.1) net-http (~> 0.3.2) - net-ldap (~> 0.17) + net-ldap (~> 0.18) nokogiri (~> 1.14) nsa (~> 0.2) oj (~> 3.14) @@ -844,24 +836,22 @@ DEPENDENCIES omniauth-cas (~> 2.0) omniauth-rails_csrf_protection (~> 0.1) omniauth-saml (~> 1.10) - omniauth_openid_connect (~> 0.6.0) + 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) - pry-byebug (~> 3.10) - pry-rails (~> 0.3) public_suffix (~> 5.0) - puma (~> 5.6) + puma (~> 6.2) pundit (~> 2.3) - rack (~> 2.2.6) + rack (~> 2.2.7) rack-attack (~> 6.6) - rack-cors (~> 1.1) - rack-test (~> 2.0) + rack-cors (~> 2.0) + rack-test (~> 2.1) rails (~> 6.1.7) rails-controller-testing (~> 1.0) rails-i18n (~> 6.0) @@ -870,16 +860,17 @@ DEPENDENCIES redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) - rexml (~> 3.2) - rqrcode (~> 2.1) - rspec-rails (~> 5.1) + rqrcode (~> 2.2) + rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) - rspec_junit_formatter (~> 0.6) + rspec_chunked (~> 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) @@ -893,11 +884,11 @@ DEPENDENCIES sprockets-rails (~> 3.4) stackprof stoplight (~> 3.0.1) - strong_migrations (~> 0.7) + strong_migrations (~> 0.8) thor (~> 1.2) tty-prompt (~> 0.23) twitter-text (~> 3.1.0) - tzinfo-data (~> 1.2022) + tzinfo-data (~> 1.2023) webauthn (~> 3.0) webmock (~> 3.18) webpacker (~> 5.4) diff --git a/README.md b/README.md index f6b67b468..50bb76527 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -# Mastodon, Queer.af+Glitch Edition # -We're a downstream of [glitch-soc](https://github.com/glitch-soc/mastodon/). You probably want that ## Updating ```bash 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/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 060db11bb..081550b76 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -2,7 +2,7 @@ module Admin class DomainBlocksController < BaseController - before_action :set_domain_block, only: [:show, :destroy, :edit, :update] + before_action :set_domain_block, only: [:destroy, :edit, :update] def batch authorize :domain_block, :create? @@ -33,7 +33,7 @@ module Admin 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 diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index a0a43de19..4a3228ec3 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -2,8 +2,6 @@ module Admin class EmailDomainBlocksController < BaseController - before_action :set_email_domain_block, only: [:show, :destroy] - def index authorize :email_domain_block, :index? @@ -59,10 +57,6 @@ module Admin private - def set_email_domain_block - @email_domain_block = EmailDomainBlock.find(params[:id]) - end - def set_resolved_records Resolv::DNS.open do |dns| dns.timeouts = 5 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..1ed3fd18a 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -10,12 +10,20 @@ 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? @@ -28,14 +36,6 @@ module Admin end end - def show - authorize @webhook, :show? - end - - def edit - authorize @webhook, :update? - end - def update authorize @webhook, :update? 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/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 94b707771..7c7d70fd3 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -13,7 +13,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def update @account = current_account UpdateAccountService.new.call(@account, account_params, raise_error: true) - UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params + current_user.update(user_params) if user_params ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end @@ -34,15 +34,17 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController ) end - def user_settings_params + def user_params return nil if params[:source].blank? source_params = params.require(:source) { - 'setting_default_privacy' => source_params.fetch(:privacy, @account.user.setting_default_privacy), - 'setting_default_sensitive' => source_params.fetch(:sensitive, @account.user.setting_default_sensitive), - 'setting_default_language' => source_params.fetch(:language, @account.user.setting_default_language), + settings_attributes: { + default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy), + default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive), + default_language: source_params.fetch(:language, @account.user.setting_default_language), + }, } end 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..8af4242ba 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 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/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 08b3474cc..76bc2b18a 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.listed.includes(:category) } end end diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c91543e3a..c0585e859 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 diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb index 32fb8e39f..29ff897b9 100644 --- a/app/controllers/api/v1/emails/confirmations_controller.rb +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -1,9 +1,10 @@ # 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 def create current_user.update!(email: params[:email]) if params.key?(:email) @@ -12,6 +13,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..5c81877bd 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 @@ -33,6 +33,6 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def featured_tag_params - params.permit(:name) + params.require(: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 new file mode 100644 index 000000000..c4680cccb --- /dev/null +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Api::V1::Instances::TranslationLanguagesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :set_languages + + vary_by '' + + def show + cache_even_if_authenticated! + render json: @languages + end + + private + + def set_languages + if TranslationService.configured? + @languages = Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { TranslationService.configured.languages } + @languages['und'] = @languages.delete(nil) if @languages.key?(nil) + else + @languages = {} + end + end +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/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..dff2425d0 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -7,6 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController before_action :set_status def show + cache_if_unauthenticated! render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer end 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/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb index b23a60170..0cdd00d62 100644 --- a/app/controllers/api/v1/streaming_controller.rb +++ b/app/controllers/api/v1/streaming_controller.rb @@ -5,7 +5,7 @@ class Api::V1::StreamingController < Api::BaseController if Rails.configuration.x.streaming_api_base_url == request.host not_found else - redirect_to streaming_api_url, status: 301 + redirect_to streaming_api_url, status: 301, allow_other_host: true end end 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/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/application_controller.rb b/app/controllers/application_controller.rb index ee3c5204d..7c09040fb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,7 +18,11 @@ class ApplicationController < ActionController::Base helper_method :current_skin helper_method :single_user_mode? helper_method :use_seamless_external_login? + 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 @@ -35,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 @@ -54,16 +60,31 @@ 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) - new_user_session_path + if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true' + '/auth/auth/openid_connect/logout' + else + new_user_session_path + end end protected @@ -116,6 +137,14 @@ class ApplicationController < ActionController::Base Devise.pam_authentication || Devise.ldap_authentication end + def omniauth_only? + ENV['OMNIAUTH_ONLY'] == 'true' + end + + def sso_account_settings + ENV.fetch('SSO_ACCOUNT_SETTINGS', nil) + end + def current_account return @current_account if defined?(@current_account) @@ -128,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 @@ -137,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..1eb318bca 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 diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 3d7962de5..9e0fb942a 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -33,7 +33,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController def after_sign_in_path_for(resource) if resource.email_present? - root_path + stored_location_for(resource) || root_path else auth_setup_path(missing_email: '1') end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 83e784e4c..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) @@ -47,7 +47,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(hash) resource.locale = I18n.locale - resource.invite_code = params[:invite_code] if resource.invite_code.blank? + resource.invite_code = @invite&.code if resource.invite_code.blank? resource.registration_form_time = session[:registration_form_time] resource.sign_up_ip = request.remote_ip @@ -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 new file mode 100644 index 000000000..5891da6f6 --- /dev/null +++ b/app/controllers/backups_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class BackupsController < ApplicationController + include RoutingHelper + + skip_before_action :require_functional! + + before_action :authenticate_user! + before_action :set_backup + + def download + case Paperclip::Attachment.default_options[:storage] + when :s3 + redirect_to @backup.dump.expiring_url(10) + when :fog + if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? + redirect_to @backup.dump.expiring_url(Time.now.utc + 10) + else + redirect_to full_asset_url(@backup.dump.url) + end + when :filesystem + redirect_to full_asset_url(@backup.dump.url) + end + end + + private + + def set_backup + @backup = current_user.backups.find(params[:id]) + end +end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 2f7d84df0..e9cff22ca 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -10,7 +10,8 @@ module AccountControllerConcern included do before_action :set_instance_presenter - before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } + + after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } end private 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 05e431b19..55ebe1bd6 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -3,8 +3,182 @@ module CacheConcern extend ActiveSupport::Concern + module ActiveRecordCoder + EMPTY_HASH = {}.freeze + + class << self + def dump(record) + instances = InstanceTracker.new + serialized_associations = serialize_associations(record, instances) + serialized_records = instances.map { |r| serialize_record(r) } + [serialized_associations, *serialized_records] + end + + def load(payload) + instances = InstanceTracker.new + serialized_associations, *serialized_records = payload + serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) } + deserialize_associations(serialized_associations, instances) + end + + private + + # Records without associations, or which have already been visited before, + # are serialized by their id alone. + # + # Records with associations are serialized as a two-element array including + # their id and the record's association cache. + # + def serialize_associations(record, instances) + return unless record + + if (id = instances.lookup(record)) + payload = id + else + payload = instances.push(record) + + cached_associations = record.class.reflect_on_all_associations.select do |reflection| + record.association_cached?(reflection.name) + end + + unless cached_associations.empty? + serialized_associations = cached_associations.map do |reflection| + association = record.association(reflection.name) + + serialized_target = if reflection.collection? + association.target.map { |target_record| serialize_associations(target_record, instances) } + else + serialize_associations(association.target, instances) + end + + [reflection.name, serialized_target] + end + + payload = [payload, serialized_associations] + end + end + + payload + end + + def deserialize_associations(payload, instances) + return unless payload + + id, associations = payload + record = instances.fetch(id) + + associations&.each do |name, serialized_target| + begin + association = record.association(name) + rescue ActiveRecord::AssociationNotFoundError + raise AssociationMissingError, "undefined association: #{name}" + end + + target = if association.reflection.collection? + serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) } + else + deserialize_associations(serialized_target, instances) + end + + association.target = target + end + + record + end + + def serialize_record(record) + arguments = [record.class.name, attributes_for_database(record)] + arguments << true if record.new_record? + arguments + end + + if Rails.gem_version >= Gem::Version.new('7.0') + def attributes_for_database(record) + attributes = record.attributes_for_database + attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } + attributes + end + else + def attributes_for_database(record) + attributes = record.instance_variable_get(:@attributes).send(:attributes).transform_values(&:value_for_database) + attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } + attributes + end + end + + def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter + begin + klass = Object.const_get(class_name) + rescue NameError + raise ClassMissingError, "undefined class: #{class_name}" + end + + # Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass + # wether the record was persisted or not. + attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH) + klass.allocate.init_with_attributes(attributes, new_record) + end + end + + class Error < StandardError + end + + class ClassMissingError < Error + end + + class AssociationMissingError < Error + end + + class InstanceTracker + def initialize + @instances = [] + @ids = {}.compare_by_identity + end + + def map(&block) + @instances.map(&block) + end + + def fetch(...) + @instances.fetch(...) + end + + def push(instance) + id = @ids[instance] = @instances.size + @instances << instance + id + end + + def lookup(instance) + @ids[instance] + end + 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 @@ -24,18 +198,19 @@ 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) raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) return [] if raw.empty? - cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) - uncached_ids = raw.map(&:id) - cached_keys_with_value.keys + cached_keys_with_value = begin + Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) } + rescue ActiveRecordCoder::Error + {} # The serialization format may have changed, let's pretend it's a cache miss. + end + + uncached_ids = raw.map(&:id) - cached_keys_with_value.keys klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) @@ -43,7 +218,7 @@ module CacheConcern uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) uncached.each_value do |item| - Rails.cache.write(item, item) + Rails.cache.write(item, ActiveRecordCoder.dump(item)) end end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index b0a087d53..1d27c92c8 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -138,7 +138,7 @@ module SignatureVerification end def signed_headers - signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split(' ') + signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split end def verify_signature_strength! @@ -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/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/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..41e20add6 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? diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 3b228722f..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,13 +16,13 @@ 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? end - redirect_to full_asset_url(@media_attachment.file.url(version)) + redirect_to full_asset_url(@media_attachment.file.url(version)), allow_other_host: true end private 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 d40770726..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? @@ -20,6 +21,8 @@ class RelationshipsController < ApplicationController @form.save rescue ActionController::ParameterMissing # Do nothing + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound + flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow' ensure redirect_to relationships_path(filter_params) end @@ -61,8 +64,8 @@ class RelationshipsController < ApplicationController 'unfollow' elsif params[:remove_from_followers] 'remove_from_followers' - elsif params[:block_domains] - 'block_domains' + elsif params[:block_domains] || params[:remove_domains_from_followers] + 'remove_domains_from_followers' end end @@ -73,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 62c52eee9..b179b9429 100644 --- a/app/controllers/settings/flavours_controller.rb +++ b/app/controllers/settings/flavours_controller.rb @@ -12,27 +12,15 @@ 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] end def update - user_settings.update(user_settings_params) + current_user.settings.update(flavour: params.require(:flavour), skin: params.dig(:user, :setting_skin)) + current_user.save redirect_to action: 'show', flavour: params[:flavour] end - - private - - def user_settings - UserSettingsDecorator.new(current_user) - end - - def user_settings_params - { setting_flavour: params.require(:flavour), - setting_skin: params.dig(:user, :setting_skin) }.with_indifferent_access - end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index d4516526e..bdbf8796f 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,31 +1,97 @@ # 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', + }.freeze + + TYPE_TO_HEADERS_MAP = { + following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], + blocking: false, + muting: ['Account address', 'Hide notifications'], + domain_blocking: false, + bookmarks: false, + }.freeze + + def index + @import = Form::Import.new(current_account: current_account) + end + + def show; end + + def failures + @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id]) + + respond_to do |format| + format.csv do + filename = TYPE_TO_FILENAME_MAP[@bulk_import.type.to_sym] + headers = TYPE_TO_HEADERS_MAP[@bulk_import.type.to_sym] + + export_data = CSV.generate(headers: headers, write_headers: true) do |csv| + @bulk_import.rows.find_each do |row| + case @bulk_import.type.to_sym + when :following + csv << [row.data['acct'], row.data.fetch('show_reblogs', true), row.data.fetch('notify', false), row.data['languages']&.join(', ')] + when :blocking + csv << [row.data['acct']] + when :muting + csv << [row.data['acct'], row.data.fetch('hide_notifications', true)] + when :domain_blocking + csv << [row.data['domain']] + when :bookmarks + csv << [row.data['uri']] + end + end + end + + send_data export_data, filename: filename + end + end + end + + def confirm + @bulk_import.update!(state: :scheduled) + BulkImportWorker.perform_async(@bulk_import.id) + redirect_to settings_imports_path, notice: I18n.t('imports.success') end 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/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb new file mode 100644 index 000000000..faf778a7e --- /dev/null +++ b/app/controllers/settings/preferences/base_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Settings::Preferences::BaseController < Settings::BaseController + def show; end + + def update + if current_user.update(user_params) + I18n.locale = current_user.locale + redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') + else + render :show + end + end + + private + + def after_update_redirect_path + raise 'Override in controller' + end + + def user_params + params.require(:user).permit(:locale, 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/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb deleted file mode 100644 index 39715b724..000000000 --- a/app/controllers/settings/preferences_controller.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class Settings::PreferencesController < Settings::BaseController - def show; end - - def update - user_settings.update(user_settings_params.to_h) - - if current_user.update(user_params) - I18n.locale = current_user.locale - redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') - else - render :show - end - end - - private - - def after_update_redirect_path - settings_preferences_path - end - - def user_settings - UserSettingsDecorator.new(current_user) - end - - def user_params - params.require(:user).permit( - :locale, - chosen_languages: [] - ) - end - - def user_settings_params - params.require(:user).permit( - :setting_default_privacy, - :setting_default_sensitive, - :setting_default_language, - :setting_unfollow_modal, - :setting_boost_modal, - :setting_favourite_modal, - :setting_delete_modal, - :setting_auto_play_gif, - :setting_display_media, - :setting_expand_spoilers, - :setting_reduce_motion, - :setting_disable_swiping, - :setting_system_font_ui, - :setting_system_emoji_font, - :setting_noindex, - :setting_hide_followers_count, - :setting_aggregate_reblogs, - :setting_show_application, - :setting_advanced_layout, - :setting_default_content_type, - :setting_use_blurhash, - :setting_use_pending_items, - :setting_trends, - :setting_crop_images, - :setting_visible_reactions, - :setting_always_send_emails, - notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag trending_link trending_status appeal), - interactions: %i(must_be_follower must_be_following must_be_following_dm) - ) - end -end 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 952c14e0b..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 @@ -52,7 +51,7 @@ module Settings end else flash[:error] = I18n.t('webauthn_credentials.create.error') - status = :internal_server_error + status = :unprocessable_entity end else flash[:error] = t('webauthn_credentials.create.error') 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 e5221df3a..575808598 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -6,14 +6,16 @@ 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 :set_link_headers before_action :redirect_to_original, only: :show - before_action :set_cache_headers before_action :set_body_classes, only: :embed + after_action :set_link_headers + skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? @@ -28,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 @@ -71,6 +73,6 @@ class StatusesController < ApplicationController end def redirect_to_original - redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? + redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog? end 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 e15aee6df..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&.setting_hide_followers_count + Setting.hide_followers_count || account.user&.settings&.[]('hide_followers_count') end def account_description(account) diff --git a/app/helpers/admin/announcements_helper.rb b/app/helpers/admin/announcements_helper.rb deleted file mode 100644 index 0c053ddec..000000000 --- a/app/helpers/admin/announcements_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Admin::AnnouncementsHelper - def time_range(announcement) - if announcement.all_day? - safe_join([l(announcement.starts_at.to_date), ' - ', l(announcement.ends_at.to_date)]) - else - safe_join([l(announcement.starts_at), ' - ', l(announcement.ends_at)]) - end - end -end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0605457e8..3192c7ab5 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 @@ -112,11 +108,15 @@ module ApplicationHelper def fa_icon(icon, attributes = {}) class_names = attributes[:class]&.split(' ') || [] class_names << 'fa' - class_names += icon.split(' ').map { |cl| "fa-#{cl}" } + class_names += icon.split.map { |cl| "fa-#{cl}" } 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']}" + URI::HTTPS.build(host: storage_host_name).to_s end def storage_host? - ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? + storage_host_name.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_name + 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/home_helper.rb b/app/helpers/home_helper.rb index f41104709..c5b83326d 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -8,7 +8,7 @@ module HomeHelper end def account_link_to(account, button = '', path: nil) - content_tag(:div, class: 'account') do + content_tag(:div, class: 'account account--minimal') do content_tag(:div, class: 'account__wrapper') do section = if account.nil? content_tag(:div, class: 'account__display-name') do @@ -41,9 +41,9 @@ module HomeHelper def obscured_counter(count) if count <= 0 - 0 + '0' elsif count == 1 - 1 + '1' else '1+' end @@ -57,14 +57,6 @@ module HomeHelper end end - def optional_link_to(condition, path, options = {}, &block) - if condition - link_to(path, options, &block) - else - content_tag(:div, &block) - end - end - def sign_up_message if closed_registrations? t('auth.registration_closed', instance: site_hostname) 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 584394758..00e1e5178 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# rubocop:disable Metrics/ModuleLength, Style/WordArray +# rubocop:disable Metrics/ModuleLength module LanguagesHelper ISO_639_1 = { @@ -201,7 +201,6 @@ module LanguagesHelper sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze, szl: ['Silesian', 'ślůnsko godka'].freeze, - tai: ['Tai', 'ภาษาไท or ภาษาไต'].freeze, tok: ['Toki Pona', 'toki pona'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, @@ -275,4 +274,4 @@ module LanguagesHelper end end -# rubocop:enable Metrics/ModuleLength, Style/WordArray +# rubocop:enable Metrics/ModuleLength 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..fc08de13d --- /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({ locale: I18n.locale }.merge(props)) } + div_tag_with_data(data) + end + + private + + def div_tag_with_data(data) + content_tag(:div, nil, data: data) + end +end 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/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..c6cf6704c 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -3,7 +3,7 @@ import 'packs/public-path'; import escapeTextContentForBrowser from 'escape-html'; -const { delegate } = require('@rails/ujs'); +import { delegate } from '@rails/ujs'; import emojify from '../mastodon/features/emoji/emoji'; diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index 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/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 6b5b2ade5..9e5c85a15 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -1,5 +1,5 @@ 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 +81,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 +844,8 @@ export function fetchPinnedAccountsFail(error) { export function fetchPinnedAccountsSuggestions(q) { return (dispatch, getState) => { + dispatch(fetchPinnedAccountsSuggestionsRequest()); + const params = { q, resolve: false, @@ -850,19 +855,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/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/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 9c0ef83df..f0d50ccf1 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -441,16 +441,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/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index f78dcec1a..fecc4d03b 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -401,7 +401,7 @@ export function unpinFail(status, error) { status, error, }; -}; +} export const addReaction = (statusId, name, url) => (dispatch, getState) => { const status = getState().get('statuses').get(statusId); @@ -474,4 +474,3 @@ export const removeReactionFail = (statusId, name) => ({ id: statusId, name, }); - diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index dfd701cbb..8fd9da405 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,6 +1,6 @@ import api from '../api'; import { debounce } from 'lodash'; -import compareId from '../compare_id'; +import { compareId } from '../compare_id'; import { List as ImmutableList } from 'immutable'; export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; @@ -55,7 +55,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/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index ecdac6256..0460d4ea2 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -13,7 +13,7 @@ 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 { compareId } from 'flavours/glitch/compare_id'; import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; 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..a0d5933b1 100644 --- a/app/javascript/flavours/glitch/actions/pin_statuses.js +++ b/app/javascript/flavours/glitch/actions/pin_statuses.js @@ -1,12 +1,12 @@ import api from '../api'; import { importFetchedStatuses } from './importer'; +import { me } from 'flavours/glitch/initial_state'; + 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/server.js b/app/javascript/flavours/glitch/actions/server.js index 31d4aea10..091af0f0f 100644 --- a/app/javascript/flavours/glitch/actions/server.js +++ b/app/javascript/flavours/glitch/actions/server.js @@ -5,6 +5,10 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL'; + export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; @@ -37,6 +41,29 @@ const fetchServerFail = error => ({ error, }); +export const fetchServerTranslationLanguages = () => (dispatch, getState) => { + dispatch(fetchServerTranslationLanguagesRequest()); + + api(getState) + .get('/api/v1/instance/translation_languages').then(({ data }) => { + dispatch(fetchServerTranslationLanguagesSuccess(data)); + }).catch(err => dispatch(fetchServerTranslationLanguagesFail(err))); +}; + +const fetchServerTranslationLanguagesRequest = () => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST, +}); + +const fetchServerTranslationLanguagesSuccess = translationLanguages => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS, + translationLanguages, +}); + +const fetchServerTranslationLanguagesFail = error => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL, + error, +}); + export const fetchExtendedDescription = () => (dispatch, getState) => { dispatch(fetchExtendedDescriptionRequest()); diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index ffac1b258..b4eb60ad4 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -27,7 +27,7 @@ const { messages } = getLocale(); /** * @param {number} max - * @return {number} + * @returns {number} */ const randomUpTo = max => Math.floor(Math.random() * Math.floor(max)); @@ -40,19 +40,22 @@ 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) => { 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 +64,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onConnect() { dispatch(connectTimeline(timelineId)); + // @ts-expect-error if (pollingId) { clearTimeout(pollingId); pollingId = null; @@ -75,6 +79,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 +87,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': @@ -115,21 +126,24 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti * @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 +153,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 +163,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/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index eb817daf9..603759760 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -2,8 +2,8 @@ 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 { compareId } from 'flavours/glitch/compare_id'; +import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; import { toServerSideType } from 'flavours/glitch/utils/filters'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; @@ -121,7 +121,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 +162,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/api.js b/app/javascript/flavours/glitch/api.js index 6bbddbef6..42b64d6cc 100644 --- a/app/javascript/flavours/glitch/api.js +++ b/app/javascript/flavours/glitch/api.js @@ -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 12096d902..000000000 --- a/app/javascript/flavours/glitch/base_polyfills.js +++ /dev/null @@ -1,47 +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 isNaN from 'is-nan'; -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(); -} - -if (!Number.isNaN) { - Number.isNaN = isNaN; -} - -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/mastodon/blurhash.js b/app/javascript/flavours/glitch/blurhash.ts similarity index 81% rename from app/javascript/mastodon/blurhash.js rename to app/javascript/flavours/glitch/blurhash.ts index 5adcc3e77..dadf2b7f2 100644 --- a/app/javascript/mastodon/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/mastodon/compare_id.js b/app/javascript/flavours/glitch/compare_id.ts similarity index 75% rename from app/javascript/mastodon/compare_id.js rename to app/javascript/flavours/glitch/compare_id.ts index d2bd74f44..30b057248 100644 --- a/app/javascript/mastodon/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 7ce4b65aa..35f83beab 100644 --- a/app/javascript/flavours/glitch/components/account.jsx +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -1,15 +1,15 @@ 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 { Avatar } from './avatar'; +import { DisplayName } from './display_name'; import Permalink from './permalink'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from 'flavours/glitch/initial_state'; -import RelativeTimestamp from './relative_timestamp'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { RelativeTimestamp } from './relative_timestamp'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -23,7 +23,6 @@ const messages = defineMessages({ block: { id: 'account.block', defaultMessage: 'Block @{name}' }, }); -export default @injectIntl class Account extends ImmutablePureComponent { static propTypes = { @@ -184,3 +183,5 @@ class Account extends ImmutablePureComponent { } } + +export default injectIntl(Account); diff --git a/app/javascript/flavours/glitch/components/admin/Counter.jsx b/app/javascript/flavours/glitch/components/admin/Counter.jsx index 5b6a19f8d..9c0b848d0 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.jsx +++ b/app/javascript/flavours/glitch/components/admin/Counter.jsx @@ -4,7 +4,7 @@ import api from 'flavours/glitch/api'; import { FormattedNumber } from 'react-intl'; import { Sparklines, SparklinesCurve } from 'react-sparklines'; import classNames from 'classnames'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; const percIncrease = (a, b) => { let percent; diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.jsx b/app/javascript/flavours/glitch/components/admin/Dimension.jsx index 3dac8c6c2..8b8466ee7 100644 --- a/app/javascript/flavours/glitch/components/admin/Dimension.jsx +++ b/app/javascript/flavours/glitch/components/admin/Dimension.jsx @@ -3,7 +3,7 @@ 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 { Skeleton } from 'flavours/glitch/components/skeleton'; export default class Dimension extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx index 771dbb452..ecefe7a84 100644 --- a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx +++ b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx @@ -33,7 +33,7 @@ class Category extends React.PureComponent { const { id, text, disabled, selected, children } = this.props; return ( -
+
{selected && }
@@ -74,7 +74,7 @@ class Rule extends React.PureComponent { const { id, text, disabled, selected } = this.props; return ( -
+
{selected && } {text} @@ -84,7 +84,6 @@ class Rule extends React.PureComponent { } -export default @injectIntl class ReportReasonSelector extends React.PureComponent { static propTypes = { @@ -157,3 +156,5 @@ class ReportReasonSelector extends React.PureComponent { } } + +export default injectIntl(ReportReasonSelector); 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..b6b073161 --- /dev/null +++ b/app/javascript/flavours/glitch/components/animated_number.tsx @@ -0,0 +1,81 @@ +import React, { useCallback, useState } 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..92ba98126 100644 --- a/app/javascript/flavours/glitch/components/attachment_list.jsx +++ b/app/javascript/flavours/glitch/components/attachment_list.jsx @@ -4,7 +4,7 @@ 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 { 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_input.jsx b/app/javascript/flavours/glitch/components/autosuggest_input.jsx index 90ff298c0..9e74590e6 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -154,7 +154,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 +180,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..fc763146e 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx @@ -153,7 +153,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 +186,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..1c23a8b36 100644 --- a/app/javascript/flavours/glitch/components/avatar_composite.jsx +++ b/app/javascript/flavours/glitch/components/avatar_composite.jsx @@ -79,15 +79,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/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..1550d0b7a --- /dev/null +++ b/app/javascript/flavours/glitch/components/blurhash.tsx @@ -0,0 +1,48 @@ +import React, { useRef, useEffect } 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/column.jsx b/app/javascript/flavours/glitch/components/column.jsx index 47293ef18..fce6a6c40 100644 --- a/app/javascript/flavours/glitch/components/column.jsx +++ b/app/javascript/flavours/glitch/components/column.jsx @@ -3,6 +3,8 @@ import PropTypes from 'prop-types'; import { supportsPassiveEvents } from 'detect-passive-events'; import { scrollTop } from '../scroll'; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + export default class Column extends React.PureComponent { static propTypes = { @@ -37,17 +39,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..003d37671 100644 --- a/app/javascript/flavours/glitch/components/column_back_button.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; import { createPortal } from 'react-dom'; export default class ColumnBackButton extends React.PureComponent { @@ -14,17 +14,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..6fb800640 100644 --- a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; export default class ColumnBackButtonSlim extends React.PureComponent { @@ -9,24 +9,22 @@ export default class ColumnBackButtonSlim extends React.PureComponent { 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 3790960dd..c4b6e9247 100644 --- a/app/javascript/flavours/glitch/components/column_header.jsx +++ b/app/javascript/flavours/glitch/components/column_header.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { createPortal } from 'react-dom'; import classNames from 'classnames'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, @@ -12,7 +12,6 @@ const messages = defineMessages({ moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, }); -export default @injectIntl class ColumnHeader extends React.PureComponent { static contextTypes = { @@ -43,20 +42,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 }); @@ -74,8 +59,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 = () => { @@ -84,8 +77,9 @@ class ColumnHeader extends React.PureComponent { handlePin = () => { if (!this.props.pinned) { - this.historyBack(); + this.context.router.history.replace('/'); } + this.props.onPin(); }; @@ -218,3 +212,5 @@ class ColumnHeader extends React.PureComponent { } } + +export default injectIntl(ColumnHeader); diff --git a/app/javascript/flavours/glitch/components/common_counter.jsx b/app/javascript/flavours/glitch/components/common_counter.jsx index dd9b62de9..9964be516 100644 --- a/app/javascript/flavours/glitch/components/common_counter.jsx +++ b/app/javascript/flavours/glitch/components/common_counter.jsx @@ -4,7 +4,6 @@ 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 c4968ac3c..2aed19b88 100644 --- a/app/javascript/flavours/glitch/components/dismissable_banner.jsx +++ b/app/javascript/flavours/glitch/components/dismissable_banner.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import IconButton from './icon_button'; +import { IconButton } from './icon_button'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; import { bannerSettings } from 'flavours/glitch/settings'; @@ -8,7 +8,6 @@ const messages = defineMessages({ dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' }, }); -export default @injectIntl class DismissableBanner extends React.PureComponent { static propTypes = { @@ -49,3 +48,5 @@ class DismissableBanner extends React.PureComponent { } } + +export default injectIntl(DismissableBanner); 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 e09fa4591..000000000 --- a/app/javascript/flavours/glitch/components/domain.jsx +++ /dev/null @@ -1,42 +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}' }, -}); - -export default @injectIntl -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} - - -
- -
-
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/domain.tsx b/app/javascript/flavours/glitch/components/domain.tsx new file mode 100644 index 000000000..9e8e04b65 --- /dev/null +++ b/app/javascript/flavours/glitch/components/domain.tsx @@ -0,0 +1,45 @@ +import React, { useCallback } from 'react'; + +import type { InjectedIntl } from 'react-intl'; +import { defineMessages, injectIntl } 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; + intl: InjectedIntl; +} +const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { + const handleDomainUnblock = useCallback(() => { + onUnblockDomain(domain); + }, [domain, onUnblockDomain]); + + return ( +
+
+ + {domain} + + +
+ +
+
+
+ ); +}; + +export const Domain = injectIntl(_Domain); diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx index f4b6e059f..901c7fe22 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx @@ -1,13 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import IconButton from './icon_button'; +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'; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; let id = 0; class DropdownMenu extends React.PureComponent { @@ -35,12 +35,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 +50,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); } @@ -119,7 +120,7 @@ class DropdownMenu extends React.PureComponent { return (
  • - + {text}
  • diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx index c973bda58..5315b812f 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx +++ b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, injectIntl } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; +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 { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; import InlineAccount from 'flavours/glitch/components/inline_account'; const mapDispatchToProps = (dispatch, { statusId }) => ({ @@ -16,8 +16,6 @@ const mapDispatchToProps = (dispatch, { statusId }) => ({ }); -export default @connect(null, mapDispatchToProps) -@injectIntl class EditedTimestamp extends React.PureComponent { static propTypes = { @@ -34,7 +32,7 @@ class EditedTimestamp extends React.PureComponent { renderHeader = items => { return ( - + ); }; @@ -68,3 +66,5 @@ class EditedTimestamp extends React.PureComponent { } } + +export default connect(null, mapDispatchToProps)(injectIntl(EditedTimestamp)); diff --git a/app/javascript/flavours/glitch/components/error_boundary.jsx b/app/javascript/flavours/glitch/components/error_boundary.jsx index 8518dfc86..234a53417 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.jsx +++ b/app/javascript/flavours/glitch/components/error_boundary.jsx @@ -72,7 +72,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 1f0f99b46..000000000 --- a/app/javascript/flavours/glitch/components/gifv.jsx +++ /dev/null @@ -1,73 +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, - 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 } = 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..c606a2904 --- /dev/null +++ b/app/javascript/flavours/glitch/components/gifv.tsx @@ -0,0 +1,70 @@ +import React, { useCallback, useState } 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..ced250b79 100644 --- a/app/javascript/flavours/glitch/components/hashtag.jsx +++ b/app/javascript/flavours/glitch/components/hashtag.jsx @@ -6,7 +6,7 @@ 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 { Skeleton } from 'flavours/glitch/components/skeleton'; import classNames from 'classnames'; class SilentErrorBoundary extends React.Component { @@ -35,13 +35,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 +49,14 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => ( /> ); +// @ts-expect-error export const ImmutableHashtag = ({ hashtag }) => ( day.get('uses')).toArray()} /> ); @@ -64,6 +65,7 @@ ImmutableHashtag.propTypes = { hashtag: ImmutablePropTypes.map.isRequired, }; +// @ts-expect-error const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
    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..6bd15da6a --- /dev/null +++ b/app/javascript/flavours/glitch/components/icon.tsx @@ -0,0 +1,22 @@ +import 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 61% rename from app/javascript/flavours/glitch/components/icon_button.jsx rename to app/javascript/flavours/glitch/components/icon_button.tsx index 10d7926be..0b8128824 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 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, @@ -141,12 +139,17 @@ 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 ( - - -
    @@ -57,3 +57,5 @@ class NotificationPurgeButtons extends ImmutablePureComponent { } } + +export default injectIntl(NotificationPurgeButtons); diff --git a/app/javascript/flavours/glitch/components/permalink.jsx b/app/javascript/flavours/glitch/components/permalink.jsx index b09b17eeb..1c5843183 100644 --- a/app/javascript/flavours/glitch/components/permalink.jsx +++ b/app/javascript/flavours/glitch/components/permalink.jsx @@ -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 8bfdf343c..e8c9611a3 100644 --- a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx @@ -1,65 +1,24 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; import { connect } from 'react-redux'; -import { debounce } from 'lodash'; import { FormattedMessage } from 'react-intl'; -export default @connect() class PictureInPicturePlaceholder extends React.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 ( -
    +
    @@ -67,3 +26,5 @@ class PictureInPicturePlaceholder extends React.PureComponent { } } + +export default connect()(PictureInPicturePlaceholder); diff --git a/app/javascript/flavours/glitch/components/poll.jsx b/app/javascript/flavours/glitch/components/poll.jsx index 53ece560e..df2391956 100644 --- a/app/javascript/flavours/glitch/components/poll.jsx +++ b/app/javascript/flavours/glitch/components/poll.jsx @@ -8,8 +8,8 @@ import Motion from 'flavours/glitch/features/ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; import escapeTextContentForBrowser from 'escape-html'; import emojify from 'flavours/glitch/features/emoji/emoji'; -import RelativeTimestamp from './relative_timestamp'; -import Icon from 'flavours/glitch/components/icon'; +import { RelativeTimestamp } from './relative_timestamp'; +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ closed: { @@ -31,7 +31,6 @@ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { return obj; }, {}); -export default @injectIntl class Poll extends ImmutablePureComponent { static contextTypes = { @@ -40,6 +39,7 @@ class Poll extends ImmutablePureComponent { static propTypes = { poll: ImmutablePropTypes.map, + lang: PropTypes.string, intl: PropTypes.object.isRequired, disabled: PropTypes.bool, refresh: PropTypes.func, @@ -126,7 +126,7 @@ class Poll extends ImmutablePureComponent { }; renderOption (option, optionIndex, showResults) { - const { poll, disabled, intl } = this.props; + const { poll, lang, disabled, intl } = this.props; const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100; const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); @@ -154,11 +154,12 @@ class Poll extends ImmutablePureComponent { {!showResults && ( )} @@ -175,6 +176,7 @@ class Poll extends ImmutablePureComponent { @@ -231,3 +233,5 @@ class Poll extends ImmutablePureComponent { } } + +export default injectIntl(Poll); 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..67acb09f4 --- /dev/null +++ b/app/javascript/flavours/glitch/components/radio_button.tsx @@ -0,0 +1,35 @@ +import 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/relative_timestamp.jsx b/app/javascript/flavours/glitch/components/relative_timestamp.jsx deleted file mode 100644 index 512480339..000000000 --- a/app/javascript/flavours/glitch/components/relative_timestamp.jsx +++ /dev/null @@ -1,199 +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; -}; - -export default @injectIntl -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 ( - - ); - } - -} 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..e0e0d4bb5 --- /dev/null +++ b/app/javascript/flavours/glitch/components/relative_timestamp.tsx @@ -0,0 +1,282 @@ +import React from 'react'; + +import type { InjectedIntl } 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: InjectedIntl, + 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: InjectedIntl, + 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: InjectedIntl; + timestamp: string; + year: number; + futureDate?: boolean; + short?: boolean; +} +interface States { + now: number; +} +class RelativeTimestamp extends React.Component { + state = { + now: this.props.intl.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: this.props.intl.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: 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 ( + + ); + } +} + +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 ae1ba3037..69925321b 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.jsx +++ b/app/javascript/flavours/glitch/components/scrollable_list.jsx @@ -8,19 +8,21 @@ import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/inters import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; import classNames from 'classnames'; +import { supportsPassiveEvents } from 'detect-passive-events'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; import LoadingIndicator from './loading_indicator'; import { connect } from 'react-redux'; const MOUSE_IDLE_DELAY = 300; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + const mapStateToProps = (state, { scrollKey }) => { return { preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']), }; }; -export default @connect(mapStateToProps, null, null, { forwardRef: true }) class ScrollableList extends PureComponent { static contextTypes = { @@ -91,15 +93,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; } }; @@ -107,6 +113,7 @@ class ScrollableList extends PureComponent { if (this.mouseIdleTimer === null) { return; } + clearTimeout(this.mouseIdleTimer); this.mouseIdleTimer = null; }; @@ -114,13 +121,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); @@ -135,6 +142,7 @@ class ScrollableList extends PureComponent { if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) { this.setScrollTop(0); } + this.mouseMovedRecently = false; this.scrollToTopOnMouseIdle = false; }; @@ -142,6 +150,7 @@ class ScrollableList extends PureComponent { componentDidMount () { this.attachScrollListener(); this.attachIntersectionObserver(); + attachFullscreenListener(this.onFullScreenChange); // Handle initial scroll position @@ -157,15 +166,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) => { @@ -174,11 +183,7 @@ class ScrollableList extends PureComponent { this.setScrollTop(newScrollTop); }; - cacheMediaWidth = (width) => { - if (width && this.state.cachedMediaWidth != width) this.setState({ cachedMediaWidth: width }); - }; - - getSnapshotBeforeUpdate (prevProps, prevState) { + getSnapshotBeforeUpdate (prevProps) { const someItemInserted = React.Children.count(prevProps.children) > 0 && React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); @@ -199,10 +204,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); } @@ -211,10 +223,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 () { @@ -224,20 +239,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); } } @@ -352,3 +367,5 @@ class ScrollableList extends PureComponent { } } + +export default connect(mapStateToProps, null, null, { forwardRef: true })(ScrollableList); diff --git a/app/javascript/flavours/glitch/components/server_banner.jsx b/app/javascript/flavours/glitch/components/server_banner.jsx index 36e0ff238..25ec40560 100644 --- a/app/javascript/flavours/glitch/components/server_banner.jsx +++ b/app/javascript/flavours/glitch/components/server_banner.jsx @@ -4,10 +4,10 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; import { fetchServer } from 'flavours/glitch/actions/server'; 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 { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; import { Link } from 'react-router-dom'; const messages = defineMessages({ @@ -18,8 +18,6 @@ const mapStateToProps = state => ({ server: state.getIn(['server', 'server']), }); -export default @connect(mapStateToProps) -@injectIntl class ServerBanner extends React.PureComponent { static propTypes = { @@ -43,7 +41,7 @@ class ServerBanner extends React.PureComponent { {domain}
    , mastodon:
    Mastodon }} />
    - +
    {isLoading ? ( @@ -91,3 +89,5 @@ class ServerBanner extends React.PureComponent { } } + +export default connect(mapStateToProps)(injectIntl(ServerBanner)); 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..973d1d1b3 --- /dev/null +++ b/app/javascript/flavours/glitch/components/server_hero_image.tsx @@ -0,0 +1,35 @@ +import React, { useCallback, useState } 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/short_number.jsx b/app/javascript/flavours/glitch/components/short_number.jsx index 535c17727..0c40941c0 100644 --- a/app/javascript/flavours/glitch/components/short_number.jsx +++ b/app/javascript/flavours/glitch/components/short_number.jsx @@ -24,7 +24,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 +31,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 +57,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 */ 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..8d43e6827 --- /dev/null +++ b/app/javascript/flavours/glitch/components/skeleton.tsx @@ -0,0 +1,12 @@ +import 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 008cd5325..522ef6a19 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -57,7 +57,6 @@ export const defaultMediaVisibility = (status, settings) => { return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); }; -export default @injectIntl class Status extends ImmutablePureComponent { static contextTypes = { @@ -70,6 +69,9 @@ class Status extends ImmutablePureComponent { id: PropTypes.string, status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, + previousId: PropTypes.string, + nextInReplyToId: PropTypes.string, + rootId: PropTypes.string, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -136,6 +138,9 @@ class Status extends ImmutablePureComponent { 'expanded', 'unread', 'pictureInPicture', + 'previousId', + 'nextInReplyToId', + 'rootId', ]; updateOnStates = [ @@ -281,7 +286,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; @@ -296,7 +301,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); } @@ -367,9 +372,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(); } @@ -389,11 +392,12 @@ class Status extends ImmutablePureComponent { handleOpenVideo = (options) => { const { status } = this.props; - this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); + this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options); }; handleOpenMedia = (media, index) => { - this.props.onOpenMedia(this.props.status.get('id'), media, index); + const { status } = this.props; + this.props.onOpenMedia(status.get('id'), media, index, status.get('language')); }; handleHotkeyOpenMedia = e => { @@ -440,16 +444,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 => { @@ -460,7 +460,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; @@ -504,7 +504,6 @@ class Status extends ImmutablePureComponent { const { handleRef, parseClick, - setExpansion, setCollapsed, } = this; const { router } = this.context; @@ -523,9 +522,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; @@ -566,10 +568,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')}
    @@ -577,7 +581,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, @@ -586,7 +594,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 07d56c7a8..cee56a239 100644 --- a/app/javascript/flavours/glitch/components/status_visibility_icon.jsx +++ b/app/javascript/flavours/glitch/components/status_visibility_icon.jsx @@ -3,7 +3,7 @@ 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' }, @@ -12,7 +12,6 @@ const messages = defineMessages({ direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); -export default @injectIntl class VisibilityIcon extends ImmutablePureComponent { static propTypes = { @@ -49,3 +48,5 @@ class VisibilityIcon extends ImmutablePureComponent { } } + +export default injectIntl(VisibilityIcon); 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..712b4c293 --- /dev/null +++ b/app/javascript/flavours/glitch/components/timeline_hint.tsx @@ -0,0 +1,27 @@ +import 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/compose_container.jsx b/app/javascript/flavours/glitch/containers/compose_container.jsx index 1e49b89a0..9b84ed583 100644 --- a/app/javascript/flavours/glitch/containers/compose_container.jsx +++ b/app/javascript/flavours/glitch/containers/compose_container.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; -import configureStore from 'flavours/glitch/store/configureStore'; +import { store } from 'flavours/glitch/store'; import { hydrateStore } from 'flavours/glitch/actions/store'; import { IntlProvider, addLocaleData } from 'react-intl'; import { getLocale } from 'mastodon/locales'; @@ -12,8 +12,6 @@ import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const store = configureStore(); - if (initialState) { store.dispatch(hydrateStore(initialState)); } diff --git a/app/javascript/flavours/glitch/containers/domain_container.jsx b/app/javascript/flavours/glitch/containers/domain_container.jsx index e92e102ab..419d5d29f 100644 --- a/app/javascript/flavours/glitch/containers/domain_container.jsx +++ b/app/javascript/flavours/glitch/containers/domain_container.jsx @@ -2,7 +2,7 @@ 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 { Domain } from '../components/domain'; import { openModal } from '../actions/modal'; const messages = defineMessages({ @@ -10,8 +10,7 @@ const messages = defineMessages({ }); const makeMapStateToProps = () => { - const mapStateToProps = (state, { }) => ({ - }); + const mapStateToProps = () => ({}); return mapStateToProps; }; diff --git a/app/javascript/flavours/glitch/containers/mastodon.jsx b/app/javascript/flavours/glitch/containers/mastodon.jsx index dd7623a81..1044fdd16 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.jsx +++ b/app/javascript/flavours/glitch/containers/mastodon.jsx @@ -5,7 +5,7 @@ import { IntlProvider, addLocaleData } from 'react-intl'; import { Provider as ReduxProvider } from 'react-redux'; import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; -import configureStore from 'flavours/glitch/store/configureStore'; +import { store } from 'flavours/glitch/store'; import UI from 'flavours/glitch/features/ui'; import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; import { hydrateStore } from 'flavours/glitch/actions/store'; @@ -20,7 +20,6 @@ addLocaleData(localeData); const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; -export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); diff --git a/app/javascript/flavours/glitch/containers/media_container.jsx b/app/javascript/flavours/glitch/containers/media_container.jsx index 37b5484e6..99e1ed55c 100644 --- a/app/javascript/flavours/glitch/containers/media_container.jsx +++ b/app/javascript/flavours/glitch/containers/media_container.jsx @@ -1,5 +1,5 @@ import React, { PureComponent, Fragment } from 'react'; -import ReactDOM from 'react-dom'; +import { createPortal } from 'react-dom'; import PropTypes from 'prop-types'; import { IntlProvider, addLocaleData } from 'react-intl'; import { fromJS } from 'immutable'; @@ -29,19 +29,20 @@ export default class MediaContainer extends PureComponent { 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 +50,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, @@ -94,7 +95,7 @@ export default class MediaContainer extends PureComponent { }), }); - return ReactDOM.createPortal( + return createPortal( , component, ); @@ -105,6 +106,7 @@ export default class MediaContainer extends PureComponent { { 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, @@ -192,7 +191,18 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onEdit (status, history) { - dispatch(editStatus(status.get('id'), history)); + 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)), + })); + } else { + dispatch(editStatus(status.get('id'), history)); + } + }); }, onTranslate (status) { @@ -211,12 +221,12 @@ 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('MEDIA', { statusId, media, index, lang })); }, - onOpenVideo (statusId, media, options) { - dispatch(openModal('VIDEO', { statusId, media, options })); + onOpenVideo (statusId, media, lang, options) { + dispatch(openModal('VIDEO', { statusId, media, lang, options })); }, onBlock (status) { diff --git a/app/javascript/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx index 1e0a8666a..8b65d9571 100644 --- a/app/javascript/flavours/glitch/features/about/index.jsx +++ b/app/javascript/flavours/glitch/features/about/index.jsx @@ -8,10 +8,10 @@ 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 { Skeleton } from 'flavours/glitch/components/skeleton'; +import { Icon } from 'flavours/glitch/components/icon'; import classNames from 'classnames'; -import Image from 'flavours/glitch/components/image'; +import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, @@ -67,7 +67,7 @@ class Section extends React.PureComponent { return (
    -
    +
    {title}
    @@ -80,8 +80,6 @@ class Section extends React.PureComponent { } -export default @connect(mapStateToProps) -@injectIntl class About extends React.PureComponent { static propTypes = { @@ -116,7 +114,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 }} />

    @@ -218,3 +216,5 @@ class About extends React.PureComponent { } } + +export default connect(mapStateToProps)(injectIntl(About)); 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 b5c0c9205..a548db269 100644 --- a/app/javascript/flavours/glitch/features/account/components/account_note.jsx +++ b/app/javascript/flavours/glitch/features/account/components/account_note.jsx @@ -3,14 +3,13 @@ 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 Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; import Textarea from 'react-textarea-autosize'; const messages = defineMessages({ placeholder: { id: 'account_note.glitch_placeholder', defaultMessage: 'No comment provided' }, }); -export default @injectIntl class Header extends ImmutablePureComponent { static propTypes = { @@ -54,11 +53,11 @@ class Header extends ImmutablePureComponent { if (isEditing) { action_buttons = (
    -
    -
    @@ -66,7 +65,7 @@ class Header extends ImmutablePureComponent { } else { action_buttons = (
    -
    @@ -102,3 +101,5 @@ class Header extends ImmutablePureComponent { } } + +export default injectIntl(Header); 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 d53080d4f..60ac06f91 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.jsx @@ -1,19 +1,13 @@ 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 { FormattedMessage, FormattedNumber } from 'react-intl'; +import { Icon } from 'flavours/glitch/components/icon'; -export default @injectIntl class ActionBar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, }; isStatusesPageActive = (match, location) => { @@ -24,7 +18,7 @@ class ActionBar extends React.PureComponent { }; render () { - const { account, intl } = this.props; + const { account } = this.props; if (account.get('suspended')) { return ( @@ -83,3 +77,5 @@ class ActionBar extends React.PureComponent { } } + +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 d646b08b2..42e0a8d2f 100644 --- a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx +++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx @@ -10,7 +10,6 @@ const messages = defineMessages({ empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' }, }); -export default @injectIntl class FeaturedTags extends ImmutablePureComponent { static contextTypes = { @@ -51,3 +50,5 @@ class FeaturedTags extends ImmutablePureComponent { } } + +export default injectIntl(FeaturedTags); 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..5c2940b74 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 @@ -2,7 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; 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 c11a472e7..a447750c2 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account/components/header.jsx @@ -6,11 +6,10 @@ 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 { Icon } from 'flavours/glitch/components/icon'; +import { IconButton } from 'flavours/glitch/components/icon_button'; +import { Avatar } from 'flavours/glitch/components/avatar'; import Button from 'flavours/glitch/components/button'; -import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container'; @@ -76,7 +75,6 @@ const dateFormatOptions = { minute: '2-digit', }; -export default @injectIntl class Header extends ImmutablePureComponent { static contextTypes = { @@ -403,3 +401,5 @@ class Header extends ImmutablePureComponent { } } + +export default injectIntl(Header); 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 17c08e375..62a459fff 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 @@ -7,7 +7,6 @@ const messages = defineMessages({ profile: { id: 'column_header.profile', defaultMessage: 'Profile' }, }); -export default @injectIntl class ProfileColumnHeader extends React.PureComponent { static propTypes = { @@ -31,3 +30,5 @@ class ProfileColumnHeader extends React.PureComponent { } } + +export default injectIntl(ProfileColumnHeader); diff --git a/app/javascript/flavours/glitch/features/account/navigation.jsx b/app/javascript/flavours/glitch/features/account/navigation.jsx index edae38ce5..b8b8e54de 100644 --- a/app/javascript/flavours/glitch/features/account/navigation.jsx +++ b/app/javascript/flavours/glitch/features/account/navigation.jsx @@ -19,7 +19,6 @@ const mapStateToProps = (state, { match: { params: { acct } } }) => { }; }; -export default @connect(mapStateToProps) class AccountNavigation extends React.PureComponent { static propTypes = { @@ -50,3 +49,5 @@ class AccountNavigation extends React.PureComponent { } } + +export default connect(mapStateToProps)(AccountNavigation); 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 d169875b0..0dfbec30c 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,6 +1,6 @@ -import Blurhash from 'flavours/glitch/components/blurhash'; +import { Blurhash } from 'flavours/glitch/components/blurhash'; import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; +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'; @@ -76,6 +76,7 @@ export default class MediaItem extends ImmutablePureComponent { {attachment.get('description')} ); @@ -95,6 +96,7 @@ export default class MediaItem extends ImmutablePureComponent { {attachment.get('description')} @@ -105,6 +107,7 @@ export default class MediaItem extends ImmutablePureComponent { className='media-gallery__item-gifv-thumbnail' aria-label={attachment.get('description')} title={attachment.get('description')} + lang={status.get('language')} role='application' src={attachment.get('url')} onMouseEnter={this.handleMouseEnter} diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.jsx b/app/javascript/flavours/glitch/features/account_gallery/index.jsx index afd6e5161..26c45eff6 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.jsx +++ b/app/javascript/flavours/glitch/features/account_gallery/index.jsx @@ -13,9 +13,10 @@ 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 { openModal } from 'flavours/glitch/actions/modal'; +import { FormattedMessage } from 'react-intl'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; const mapStateToProps = (state, { params: { acct, id } }) => { const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); @@ -58,7 +59,6 @@ class LoadMoreMedia extends ImmutablePureComponent { } -export default @connect(mapStateToProps) class AccountGallery extends ImmutablePureComponent { static propTypes = { @@ -72,8 +72,8 @@ class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, - multiColumn: PropTypes.bool, suspended: PropTypes.bool, + multiColumn: PropTypes.bool, }; state = { @@ -142,16 +142,17 @@ 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('VIDEO', { media: attachment, statusId, lang, options: { autoPlay: true } })); } else if (attachment.get('type') === 'audio') { - dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal('AUDIO', { 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('MEDIA', { media, index, statusId, lang })); } }; @@ -167,9 +168,7 @@ class AccountGallery extends ImmutablePureComponent { if (!isAccount) { return ( - - - + ); } @@ -223,3 +222,5 @@ class AccountGallery extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(AccountGallery); 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..9b3c273bc 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx @@ -4,6 +4,7 @@ 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 MemorialNote from './memorial_note'; import { FormattedMessage } from 'react-intl'; import { NavLink } from 'react-router-dom'; import MovedNote from './moved_note'; @@ -116,6 +117,7 @@ export default class Header extends ImmutablePureComponent { return (
    + {(!hidden && account.get('memorial')) && } {(!hidden && account.get('moved')) && } ({ }); -export default @connect(() => {}, mapDispatchToProps) class LimitedAccountHint extends React.PureComponent { static propTypes = { @@ -34,3 +33,5 @@ class LimitedAccountHint extends React.PureComponent { } } + +export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint); 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..fed95ac2a --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/components/memorial_note.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +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..7d025ed41 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 @@ -4,8 +4,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; 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'; +import { Icon } from 'flavours/glitch/components/icon'; export default class MovedNote extends ImmutablePureComponent { @@ -21,9 +21,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(); 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..c5432c64f 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 @@ -93,10 +93,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 })); diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.jsx b/app/javascript/flavours/glitch/features/account_timeline/index.jsx index 9151c1990..b90918a01 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/index.jsx @@ -3,22 +3,21 @@ 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 { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../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 { TimelineHint } from 'flavours/glitch/components/timeline_hint'; import LimitedAccountHint from './components/limited_account_hint'; import { getAccountHidden } from 'flavours/glitch/selectors'; -import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; import { fetchFeaturedTags } from '../../actions/featured_tags'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; const emptyList = ImmutableList(); @@ -62,7 +61,6 @@ RemoteHint.propTypes = { url: PropTypes.string.isRequired, }; -export default @connect(mapStateToProps) class AccountTimeline extends ImmutablePureComponent { static propTypes = { @@ -124,7 +122,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) { @@ -161,10 +159,7 @@ class AccountTimeline extends ImmutablePureComponent { ); } else if (!isLoading && !isAccount) { return ( - - - - + ); } @@ -207,3 +202,5 @@ class AccountTimeline extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(AccountTimeline); diff --git a/app/javascript/flavours/glitch/features/audio/index.jsx b/app/javascript/flavours/glitch/features/audio/index.jsx index c4aa98962..25252c01c 100644 --- a/app/javascript/flavours/glitch/features/audio/index.jsx +++ b/app/javascript/flavours/glitch/features/audio/index.jsx @@ -2,12 +2,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video'; -import Icon from 'flavours/glitch/components/icon'; +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 { Blurhash } from 'flavours/glitch/components/blurhash'; import { is } from 'immutable'; const messages = defineMessages({ @@ -22,12 +22,12 @@ const messages = defineMessages({ const TICK_SIZE = 10; const PADDING = 180; -export default @injectIntl class Audio extends React.PureComponent { static propTypes = { src: PropTypes.string.isRequired, alt: PropTypes.string, + lang: PropTypes.string, poster: PropTypes.string, duration: PropTypes.number, width: PropTypes.number, @@ -94,7 +94,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 +142,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 +390,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 +402,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 () { @@ -464,7 +464,7 @@ class Audio extends React.PureComponent { }; render () { - const { src, intl, alt, editable, autoPlay, sensitive, blurhash } = this.props; + const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props; const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); @@ -476,7 +476,7 @@ class Audio extends React.PureComponent { } return ( -
    +
    @@ -520,9 +521,16 @@ class Audio extends React.PureComponent { {(revealed || editable) && }
    @@ -531,7 +539,7 @@ class Audio extends React.PureComponent { @@ -548,7 +556,7 @@ class Audio extends React.PureComponent {
    @@ -573,3 +581,5 @@ class Audio extends React.PureComponent { } } + +export default injectIntl(Audio); diff --git a/app/javascript/flavours/glitch/features/blocks/index.jsx b/app/javascript/flavours/glitch/features/blocks/index.jsx index 4461bd14d..669425c7f 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.jsx +++ b/app/javascript/flavours/glitch/features/blocks/index.jsx @@ -22,8 +22,6 @@ const mapStateToProps = state => ({ isLoading: state.getIn(['user_lists', 'blocks', 'isLoading'], true), }); -export default @connect(mapStateToProps) -@injectIntl class Blocks extends ImmutablePureComponent { static propTypes = { @@ -36,7 +34,7 @@ class Blocks extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBlocks()); } @@ -77,3 +75,5 @@ class Blocks extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(injectIntl(Blocks)); diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx index 8e25bc6fd..d9c074682 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx @@ -22,8 +22,6 @@ const mapStateToProps = state => ({ hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), }); -export default @connect(mapStateToProps) -@injectIntl class Bookmarks extends ImmutablePureComponent { static propTypes = { @@ -36,7 +34,7 @@ class Bookmarks extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBookmarkedStatuses()); } @@ -106,3 +104,5 @@ class Bookmarks extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(injectIntl(Bookmarks)); 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 bdaa9885c..1f17ea9cf 100644 --- a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx +++ b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx @@ -9,7 +9,6 @@ const mapStateToProps = state => ({ message: state.getIn(['server', 'server', 'registrations', 'message']), }); -export default @connect(mapStateToProps) class ClosedRegistrationsModal extends ImmutablePureComponent { componentDidMount () { @@ -73,3 +72,5 @@ class ClosedRegistrationsModal extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(ClosedRegistrationsModal); 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 69a4699ac..0ea874e95 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 @@ -10,7 +10,6 @@ const messages = defineMessages({ settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -export default @injectIntl class ColumnSettings extends React.PureComponent { static propTypes = { @@ -39,3 +38,5 @@ class ColumnSettings extends React.PureComponent { } } + +export default injectIntl(ColumnSettings); diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.jsx b/app/javascript/flavours/glitch/features/community_timeline/index.jsx index b9a59fdc7..8f3e10fe9 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/community_timeline/index.jsx @@ -32,8 +32,6 @@ const mapStateToProps = (state, { columnId }) => { }; }; -export default @connect(mapStateToProps) -@injectIntl class CommunityTimeline extends React.PureComponent { static defaultProps = { @@ -162,3 +160,5 @@ class CommunityTimeline extends React.PureComponent { } } + +export default connect(mapStateToProps)(injectIntl(CommunityTimeline)); 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 1843fdacb..af1f02efc 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -21,7 +21,6 @@ const messages = defineMessages({ bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, }); -export default @injectIntl class ActionBar extends React.PureComponent { static propTypes = { @@ -66,3 +65,5 @@ class ActionBar extends React.PureComponent { } } + +export default injectIntl(ActionBar); 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..db17c6e72 100644 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx @@ -1,6 +1,6 @@ import React from 'react'; -import Avatar from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; +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'; 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 696188f31..77062b813 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import CharacterCounter from './character_counter'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; @@ -11,13 +12,12 @@ 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 { length } from 'stringz'; import { countableText } from '../util/counter'; +import { maxChars } from 'flavours/glitch/initial_state'; import OptionsContainer from '../containers/options_container'; 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?' }, @@ -32,7 +32,6 @@ const messages = defineMessages({ spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, }); -export default @injectIntl class ComposeForm extends ImmutablePureComponent { static contextTypes = { @@ -77,7 +76,6 @@ class ComposeForm extends ImmutablePureComponent { preselectOnReply: PropTypes.bool, onChangeSpoilerness: PropTypes.func, onChangeVisibility: PropTypes.func, - onPaste: PropTypes.func, onMediaDescriptionConfirm: PropTypes.func, }; @@ -165,11 +163,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']); }; @@ -178,7 +176,7 @@ class ComposeForm extends ImmutablePureComponent { this.handleSubmit(); } - if (e.keyCode == 13 && e.altKey) { + if (e.keyCode === 13 && e.altKey) { this.handleSecondarySubmit(); } }; @@ -282,9 +280,7 @@ class ComposeForm extends ImmutablePureComponent { const { handleEmojiPick, handleSecondarySubmit, - handleSelect, handleSubmit, - handleRefTextarea, } = this; const { advancedOptions, @@ -292,7 +288,6 @@ class ComposeForm extends ImmutablePureComponent { isSubmitting, layout, onChangeSpoilerness, - onChangeVisibility, onClearSuggestions, onFetchSuggestions, onPaste, @@ -323,10 +318,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' @@ -343,11 +338,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} @@ -389,3 +384,5 @@ class ComposeForm extends ImmutablePureComponent { } } + +export default injectIntl(ComposeForm); diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx index fe4ab36f5..e791faa6b 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx @@ -5,12 +5,9 @@ import React from 'react'; 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 { @@ -50,7 +47,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 +56,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..28f316619 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx @@ -1,15 +1,13 @@ // Package imports. import PropTypes from 'prop-types'; import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; import classNames from 'classnames'; +import { supportsPassiveEvents } from 'detect-passive-events'; // Components. -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; -// Utils. -import { withPassive } from 'flavours/glitch/utils/dom_helpers'; -import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; // The component. export default class ComposerOptionsDropdownContent extends React.PureComponent { @@ -43,6 +41,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 +52,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 +63,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 +77,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 +131,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(); } @@ -169,7 +169,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 +184,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 cdbd957c0..b634dc780 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 @@ -28,7 +28,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`; @@ -60,7 +60,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 +79,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); } @@ -145,8 +145,7 @@ class ModifierPicker extends React.PureComponent { } -@injectIntl -class EmojiPickerMenu extends React.PureComponent { +class EmojiPickerMenuImpl extends React.PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, @@ -178,7 +177,7 @@ class EmojiPickerMenu 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 @@ -193,7 +192,7 @@ class EmojiPickerMenu extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -307,7 +306,8 @@ class EmojiPickerMenu extends React.PureComponent { } -export default @injectIntl +const EmojiPickerMenu = injectIntl(EmojiPickerMenuImpl); + class EmojiPickerDropdown extends React.PureComponent { static propTypes = { @@ -390,7 +390,7 @@ class EmojiPickerDropdown extends React.PureComponent { />}
    - + {({ props, placement })=> (
    @@ -412,3 +412,5 @@ class EmojiPickerDropdown extends React.PureComponent { } } + +export default injectIntl(EmojiPickerDropdown); diff --git a/app/javascript/flavours/glitch/features/compose/components/header.jsx b/app/javascript/flavours/glitch/features/compose/components/header.jsx index dcbdafa57..0a94ca5fc 100644 --- a/app/javascript/flavours/glitch/features/compose/components/header.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/header.jsx @@ -7,7 +7,7 @@ import { Link } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; // Components. -import Icon from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; // Utils. import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; @@ -45,7 +45,6 @@ const messages = defineMessages({ }, }); -export default @injectIntl class Header extends ImmutablePureComponent { static propTypes = { @@ -78,7 +77,7 @@ class Header extends ImmutablePureComponent { // The result. return ( -