diff --git a/.browserslistrc b/.browserslistrc index 0135379d6e..6367e4d358 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,6 +1,10 @@ +[production] defaults > 0.2% firefox >= 78 ios >= 15.6 not dead not OperaMini all + +[development] +supports es6-module diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml index 5da1ec3a24..705d26e0ab 100644 --- a/.devcontainer/compose.yaml +++ b/.devcontainer/compose.yaml @@ -10,7 +10,6 @@ services: RAILS_ENV: development NODE_ENV: development BIND: 0.0.0.0 - BOOTSNAP_CACHE_DIR: /tmp REDIS_HOST: redis REDIS_PORT: '6379' DB_HOST: db @@ -21,13 +20,12 @@ services: ES_HOST: es ES_PORT: '9200' LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 - LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000} # Overrides default command so things don't shut down after the process ends. command: sleep infinity ports: - - '3000:3000' - - '3035:3035' - - '4000:4000' + - '127.0.0.1:3000:3000' + - '127.0.0.1:3035:3035' + - '127.0.0.1:4000:4000' networks: - external_network - internal_network diff --git a/.dockerignore b/.dockerignore index 9d990ab9ce..41da718049 100644 --- a/.dockerignore +++ b/.dockerignore @@ -20,9 +20,3 @@ postgres14 redis elasticsearch chart -.yarn/ -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions diff --git a/.env.production.sample b/.env.production.sample index a311ad5f8d..3dd66abae4 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -50,7 +50,7 @@ OTP_SECRET= # Must be available (and set to same values) for all server processes # These are private/secret values, do not share outside hosting environment # Use `bin/rails db:encryption:init` to generate fresh secrets -# Do NOT change these secrets once in use, as this would cause data loss and other issues +# Do not change these secrets once in use, as this would cause data loss and other issues # ------------------ # ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY= # ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT= @@ -79,9 +79,6 @@ AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= S3_ALIAS_HOST=files.example.com -# Optional list of hosts that are allowed to serve media for your instance -# EXTRA_MEDIA_HOSTS=https://data.example1.com,https://data.example2.com - # IP and session retention # ----------------------- # Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml @@ -89,27 +86,3 @@ S3_ALIAS_HOST=files.example.com # ----------------------- IP_RETENTION_PERIOD=31556952 SESSION_RETENTION_PERIOD=31556952 - -# Fetch All Replies Behavior -# -------------------------- -# When a user expands a post (DetailedStatus view), fetch all of its replies -# (default: false) -FETCH_REPLIES_ENABLED=false - -# Period to wait between fetching replies (in minutes) -FETCH_REPLIES_COOLDOWN_MINUTES=15 - -# Period to wait after a post is first created before fetching its replies (in minutes) -FETCH_REPLIES_INITIAL_WAIT_MINUTES=5 - -# Max number of replies to fetch - total, recursively through a whole reply tree -FETCH_REPLIES_MAX_GLOBAL=1000 - -# Max number of replies to fetch - for a single post -FETCH_REPLIES_MAX_SINGLE=500 - -# Max number of replies Collection pages to fetch - total -FETCH_REPLIES_MAX_PAGES=500 - -# Maximum allowed character count -MAX_CHARS=5555 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..d4930e1f52 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,13 @@ +/build/** +/coverage/** +/db/** +/lib/** +/log/** +/node_modules/** +/nonobox/** +/public/** +!/public/embed.js +/spec/** +/tmp/** +/vendor/** +!.eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..93ff1d7b59 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,367 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config'); + +module.exports = defineConfig({ + root: true, + + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/recommended', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended', + ], + + env: { + browser: true, + node: true, + es6: true, + }, + + parser: '@typescript-eslint/parser', + + plugins: [ + 'react', + 'jsx-a11y', + 'import', + 'promise', + '@typescript-eslint', + 'formatjs', + ], + + parserOptions: { + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2021, + requireConfigFile: false, + babelOptions: { + configFile: false, + presets: ['@babel/react', '@babel/env'], + }, + }, + + settings: { + react: { + version: 'detect', + }, + 'import/ignore': [ + 'node_modules', + '\\.(css|scss|json)$', + ], + 'import/resolver': { + typescript: {}, + }, + }, + + rules: { + 'consistent-return': 'error', + 'dot-notation': 'error', + eqeqeq: ['error', 'always', { 'null': 'ignore' }], + 'indent': ['error', 2], + 'jsx-quotes': ['error', 'prefer-single'], + 'semi': ['error', 'always'], + 'no-catch-shadow': 'error', + 'no-console': [ + 'warn', + { + allow: [ + 'error', + 'warn', + ], + }, + ], + 'no-empty': ['error', { "allowEmptyCatch": true }], + 'no-restricted-properties': [ + 'error', + { property: 'substring', message: 'Use .slice instead of .substring.' }, + { property: 'substr', message: 'Use .slice instead of .substr.' }, + ], + 'no-restricted-syntax': [ + 'error', + { + // eslint-disable-next-line no-restricted-syntax + selector: 'Literal[value=/•/], JSXText[value=/•/]', + // eslint-disable-next-line no-restricted-syntax + message: "Use '·' (middle dot) instead of '•' (bullet)", + }, + ], + 'no-unused-expressions': 'error', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'after-used', + destructuredArrayIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + 'valid-typeof': 'error', + + 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }], + 'react/jsx-boolean-value': 'error', + 'react/display-name': 'off', + 'react/jsx-fragments': ['error', 'syntax'], + 'react/jsx-equals-spacing': 'error', + 'react/jsx-no-bind': 'error', + 'react/jsx-no-useless-fragment': 'error', + 'react/jsx-no-target-blank': 'off', + 'react/jsx-tag-spacing': 'error', + 'react/jsx-uses-react': 'off', // not needed with new JSX transform + 'react/jsx-wrap-multilines': 'error', + 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform + 'react/self-closing-comp': 'error', + + // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46 + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/label-has-associated-control': 'off', + 'jsx-a11y/media-has-caption': 'off', + 'jsx-a11y/no-autofocus': 'off', + // recommended rule is: + // 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ + // 'error', + // { + // tr: ['none', 'presentation'], + // canvas: ['img'], + // }, + // ], + 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off', + // recommended rule is: + // 'jsx-a11y/no-noninteractive-tabindex': [ + // 'error', + // { + // tags: [], + // roles: ['tabpanel'], + // allowExpressionValues: true, + // }, + // ], + 'jsx-a11y/no-noninteractive-tabindex': 'off', + // recommended is full 'error' + 'jsx-a11y/no-static-element-interactions': [ + 'warn', + { + handlers: [ + 'onClick', + ], + }, + ], + + // See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js + 'import/extensions': [ + 'error', + 'always', + { + 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: [ + '.eslintrc.js', + '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', + + 'import/order': [ + 'error', + { + alphabetize: { order: 'asc' }, + 'newlines-between': 'always', + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + ['index', 'sibling'], + 'object', + ], + pathGroups: [ + // React core packages + { + pattern: '{react,react-dom,react-dom/client,prop-types}', + group: 'builtin', + position: 'after', + }, + // I18n + { + pattern: '{react-intl,intl-messageformat}', + group: 'builtin', + position: 'after', + }, + // Common React utilities + { + pattern: '{classnames,react-helmet,react-router,react-router-dom}', + group: 'external', + position: 'before', + }, + // Immutable / Redux / data store + { + pattern: '{immutable,@reduxjs/toolkit,react-redux,react-immutable-proptypes,react-immutable-pure-component}', + group: 'external', + position: 'before', + }, + // Internal packages + { + pattern: '{mastodon/**}', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: [], + }, + ], + + 'promise/always-return': 'off', + 'promise/catch-or-return': [ + 'error', + { + allowFinally: true, + }, + ], + '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-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: [ + '.eslintrc.js', + '*.config.js', + '.*rc.js', + 'ide-helper.js', + 'config/webpack/**/*', + 'config/formatjs-formatter.js', + ], + + env: { + commonjs: true, + }, + + parserOptions: { + sourceType: 'script', + }, + + rules: { + 'import/no-commonjs': 'off', + }, + }, + { + files: [ + '**/*.ts', + '**/*.tsx', + ], + + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/strict-type-checked', + 'plugin:@typescript-eslint/stylistic-type-checked', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended-typescript', + ], + + parserOptions: { + projectService: true, + tsconfigRootDir: __dirname, + }, + + rules: { + // Disable formatting rules that have been enabled in the base config + 'indent': 'off', + + // This is not needed as we use noImplicitReturns, which handles this in addition to understanding types + 'consistent-return': 'off', + + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + + '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + "@typescript-eslint/prefer-nullish-coalescing": ['error', { ignorePrimitives: { boolean: true } }], + "@typescript-eslint/no-restricted-imports": [ + "warn", + { + "name": "react-redux", + "importNames": ["useSelector", "useDispatch"], + "message": "Use typed hooks `useAppDispatch` and `useAppSelector` instead." + } + ], + "@typescript-eslint/restrict-template-expressions": ['warn', { allowNumber: true }], + 'jsdoc/require-jsdoc': 'off', + + // Those rules set stricter rules for TS files + // to enforce better practices when converting from JS + 'import/no-default-export': 'warn', + 'react/prefer-stateless-function': 'warn', + 'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }], + 'react/jsx-uses-react': 'off', // not needed with new JSX transform + 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform + 'react/prop-types': 'off', + }, + }, + { + files: [ + '**/__tests__/*.js', + '**/__tests__/*.jsx', + ], + + env: { + jest: true, + }, + } + ], +}); diff --git a/.github/ISSUE_TEMPLATE/3.troubleshooting.yml b/.github/ISSUE_TEMPLATE/3.troubleshooting.yml index fa9bfc7c80..eeb74b160b 100644 --- a/.github/ISSUE_TEMPLATE/3.troubleshooting.yml +++ b/.github/ISSUE_TEMPLATE/3.troubleshooting.yml @@ -61,7 +61,7 @@ body: value: | Please at least include those informations: - Operating system: (eg. Ubuntu 22.04) - - Ruby version: (from `ruby --version`, eg. v3.4.1) + - Ruby version: (from `ruby --version`, eg. v3.3.5) - Node.js version: (from `node --version`, eg. v20.18.0) validations: required: false diff --git a/.github/renovate.json5 b/.github/renovate.json5 index e638b9c548..8a10676283 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -15,8 +15,6 @@ // to `null` after any other rule set it to something. dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).', postUpdateOptions: ['yarnDedupeHighest'], - // The types are now included in recent versions,we ignore them here until we upgrade and remove the dependency - ignoreDeps: ['@types/emoji-mart'], packageRules: [ { // Require Dependency Dashboard Approval for major version bumps of these node packages @@ -99,13 +97,7 @@ { // Group all eslint-related packages with `eslint` in the same PR matchManagers: ['npm'], - matchPackageNames: [ - 'eslint', - 'eslint-*', - 'typescript-eslint', - '@eslint/*', - 'globals', - ], + matchPackageNames: ['eslint', 'eslint-*', '@typescript-eslint/*'], matchUpdateTypes: ['patch', 'minor'], groupName: 'eslint (non-major)', }, diff --git a/.github/workflows/build-security.yml b/.github/workflows/build-security.yml index d3cb4e5e0a..1e2455d3d9 100644 --- a/.github/workflows/build-security.yml +++ b/.github/workflows/build-security.yml @@ -24,6 +24,8 @@ jobs: uses: ./.github/workflows/build-container-image.yml with: file_to_build: Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true cache: false push_to_images: | tootsuite/mastodon @@ -44,6 +46,8 @@ jobs: uses: ./.github/workflows/build-container-image.yml with: file_to_build: streaming/Dockerfile + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true cache: false push_to_images: | tootsuite/mastodon-streaming diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index 63529e4f16..ee36acfb97 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -24,7 +24,7 @@ permissions: jobs: check-i18n: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/crowdin-download-stable.yml b/.github/workflows/crowdin-download-stable.yml index 6d9a058629..ef28258cca 100644 --- a/.github/workflows/crowdin-download-stable.yml +++ b/.github/workflows/crowdin-download-stable.yml @@ -50,7 +50,7 @@ jobs: # Create or update the pull request - name: Create Pull Request - uses: peter-evans/create-pull-request@v7.0.6 + uses: peter-evans/create-pull-request@v7.0.5 with: commit-message: 'New Crowdin translations' title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)' diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index ffab4880e1..f379c56112 100644 --- a/.github/workflows/lint-css.yml +++ b/.github/workflows/lint-css.yml @@ -43,4 +43,4 @@ jobs: uses: ./.github/actions/setup-javascript - name: Stylelint - run: yarn lint:css --custom-formatter @csstools/stylelint-formatter-github + run: yarn lint:css -f github diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 13468e7799..621a662387 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -14,7 +14,7 @@ on: - 'tsconfig.json' - '.nvmrc' - '.prettier*' - - 'eslint.config.mjs' + - '.eslint*' - '**/*.js' - '**/*.jsx' - '**/*.ts' @@ -28,7 +28,7 @@ on: - 'tsconfig.json' - '.nvmrc' - '.prettier*' - - 'eslint.config.mjs' + - '.eslint*' - '**/*.js' - '**/*.jsx' - '**/*.ts' @@ -47,7 +47,7 @@ jobs: uses: ./.github/actions/setup-javascript - name: ESLint - run: yarn workspaces foreach --all --parallel run lint:js --max-warnings 0 + run: yarn lint:js --max-warnings 0 - name: Typecheck run: yarn typecheck diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml index 5bb67b108c..1d4395e9ac 100644 --- a/.github/workflows/lint-ruby.yml +++ b/.github/workflows/lint-ruby.yml @@ -12,7 +12,6 @@ on: - 'Gemfile*' - '.rubocop*.yml' - '.ruby-version' - - 'bin/rubocop' - 'config/brakeman.ignore' - '**/*.rb' - '**/*.rake' @@ -23,7 +22,6 @@ on: - 'Gemfile*' - '.rubocop*.yml' - '.ruby-version' - - 'bin/rubocop' - 'config/brakeman.ignore' - '**/*.rb' - '**/*.rake' diff --git a/.github/workflows/test-migrations.yml b/.github/workflows/test-migrations.yml index c4a716e8f9..892f59f941 100644 --- a/.github/workflows/test-migrations.yml +++ b/.github/workflows/test-migrations.yml @@ -15,7 +15,6 @@ on: - '**/*.rb' - '.github/workflows/test-migrations.yml' - 'lib/tasks/tests.rake' - - 'lib/tasks/db.rake' pull_request: paths: @@ -67,6 +66,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres + DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_CLEAN: true BUNDLE_FROZEN: true @@ -80,18 +80,6 @@ jobs: - name: Set up Ruby environment uses: ./.github/actions/setup-ruby - - name: Ensure no errors with `db:prepare` - run: | - bin/rails db:drop - bin/rails db:prepare - bin/rails db:migrate - - - name: Ensure no errors with `db:prepare` and SKIP_POST_DEPLOYMENT_MIGRATIONS - run: | - bin/rails db:drop - SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:prepare - bin/rails db:migrate - - name: Test "one step migration" flow run: | bin/rails db:drop @@ -105,11 +93,6 @@ jobs: bin/rails db:drop bin/rails db:create SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database - - # Migrate up to v4.2.0 breakpoint - bin/rails db:migrate VERSION=20230907150100 - - # Migrate the rest SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate bin/rails db:migrate bin/rails tests:migrations:check_database diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index fd4c666059..d8e6cd11d2 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -110,7 +110,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }} + DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} RAILS_ENV: test ALLOW_NOPAM: true PAM_ENABLED: true @@ -129,7 +129,6 @@ jobs: matrix: ruby-version: - '3.2' - - '3.3' - '.ruby-version' steps: - uses: actions/checkout@v4 @@ -171,7 +170,7 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v4 with: files: coverage/lcov/*.lcov env: @@ -179,7 +178,7 @@ jobs: test-libvips: name: Libvips tests - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: - build @@ -212,7 +211,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres - COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }} + DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} RAILS_ENV: test ALLOW_NOPAM: true PAM_ENABLED: true @@ -231,7 +230,6 @@ jobs: matrix: ruby-version: - '3.2' - - '3.3' - '.ruby-version' steps: - uses: actions/checkout@v4 @@ -258,7 +256,7 @@ jobs: - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v4 with: files: coverage/lcov/mastodon.lcov env: @@ -299,6 +297,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres + DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_WITH: test ES_ENABLED: false @@ -310,7 +309,6 @@ jobs: matrix: ruby-version: - '3.2' - - '3.3' - '.ruby-version' steps: @@ -415,6 +413,7 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASS: postgres + DISABLE_SIMPLECOV: true RAILS_ENV: test BUNDLE_WITH: test ES_ENABLED: true @@ -426,7 +425,6 @@ jobs: matrix: ruby-version: - '3.2' - - '3.3' - '.ruby-version' search-image: - docker.elastic.co/elasticsearch/elasticsearch:7.17.13 diff --git a/.nvmrc b/.nvmrc index 744ca17ec0..8b84b727be 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.14 +22.11 diff --git a/.prettierignore b/.prettierignore index 80b4c0159e..6b2f0c1889 100644 --- a/.prettierignore +++ b/.prettierignore @@ -63,7 +63,6 @@ docker-compose.override.yml # Ignore emoji map file /app/javascript/mastodon/features/emoji/emoji_map.json -/app/javascript/mastodon/features/emoji/emoji_sheet.json # Ignore locale files /app/javascript/mastodon/locales/*.json diff --git a/.prettierrc.js b/.prettierrc.js index 65ec869c33..af39b253f6 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,4 +1,4 @@ module.exports = { singleQuote: true, jsxSingleQuote: true -}; +} diff --git a/.rubocop.yml b/.rubocop.yml index 1bbba515af..342cf1dcb5 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,7 +18,6 @@ inherit_from: - .rubocop/rspec_rails.yml - .rubocop/rspec.yml - .rubocop/style.yml - - .rubocop/i18n.yml - .rubocop/custom.yml - .rubocop_todo.yml - .rubocop/strict.yml @@ -27,10 +26,10 @@ inherit_mode: merge: - Exclude -plugins: - - rubocop-capybara - - rubocop-i18n - - rubocop-performance +require: - rubocop-rails - rubocop-rspec - rubocop-rspec_rails + - rubocop-performance + - rubocop-capybara + - ./lib/linter/rubocop_middle_dot diff --git a/.rubocop/i18n.yml b/.rubocop/i18n.yml deleted file mode 100644 index de395d3a79..0000000000 --- a/.rubocop/i18n.yml +++ /dev/null @@ -1,12 +0,0 @@ -I18n/RailsI18n: - Enabled: true - Exclude: - - 'config/**/*' - - 'db/**/*' - - 'lib/**/*' - - 'spec/**/*' -I18n/GetText: - Enabled: false - -I18n/RailsI18n/DecorateStringFormattingUsingInterpolation: - Enabled: false diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml index bbd172e656..ae31c1f266 100644 --- a/.rubocop/rails.yml +++ b/.rubocop/rails.yml @@ -2,9 +2,6 @@ Rails/BulkChangeTable: Enabled: false # Conflicts with strong_migrations features -Rails/Delegate: - Enabled: false - Rails/FilePath: EnforcedStyle: arguments diff --git a/.rubocop/style.yml b/.rubocop/style.yml index f59340d452..03e35a70ac 100644 --- a/.rubocop/style.yml +++ b/.rubocop/style.yml @@ -1,7 +1,4 @@ --- -Style/ArrayIntersect: - Enabled: false - Style/ClassAndModuleChildren: Enabled: false @@ -22,13 +19,6 @@ Style/HashSyntax: EnforcedShorthandSyntax: either EnforcedStyle: ruby19_no_mixed_keys -Style/IfUnlessModifier: - Exclude: - - '**/*.haml' - -Style/KeywordArgumentsMerging: - Enabled: false - Style/NumericLiterals: AllowedPatterns: - \d{4}_\d{2}_\d{2}_\d{6} @@ -47,9 +37,6 @@ Style/RedundantFetchBlock: Style/RescueStandardError: EnforcedStyle: implicit -Style/SafeNavigationChainLength: - Enabled: false - Style/SymbolArray: Enabled: false @@ -58,6 +45,3 @@ Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: comma - -Style/WordArray: - MinSize: 3 # Override default of 2 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 13fb25d333..cd5d365c67 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.75.2. +# using RuboCop version 1.66.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -8,7 +8,7 @@ Lint/NonLocalExitFromIterator: Exclude: - - 'app/helpers/json_ld_helper.rb' + - 'app/helpers/jsonld_helper.rb' # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: @@ -39,6 +39,7 @@ Rails/OutputSafety: # Configuration parameters: AllowedVars. Style/FetchEnvVar: Exclude: + - 'app/lib/translation_service.rb' - 'config/environments/production.rb' - 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/3_omniauth.rb' @@ -49,7 +50,7 @@ Style/FetchEnvVar: - 'lib/tasks/repo.rake' # This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns. +# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. # SupportedStyles: annotated, template, unannotated # AllowedMethods: redirect Style/FormatStringToken: @@ -62,10 +63,31 @@ Style/FormatStringToken: Style/GuardClause: Enabled: false +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/HashTransformValues: + Exclude: + - 'app/serializers/rest/web_push_subscription_serializer.rb' + - 'app/services/import_service.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapToHash: + Exclude: + - 'app/models/status.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: literals, strict +Style/MutableConstant: + Exclude: + - 'app/models/tag.rb' + - 'app/services/delete_account_service.rb' + - 'lib/mastodon/migration_warning.rb' + # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: + - 'app/helpers/jsonld_helper.rb' - 'app/lib/admin/system_check/message.rb' - 'app/lib/request.rb' - 'app/lib/webfinger.rb' @@ -86,3 +108,10 @@ Style/RedundantConstantBase: Exclude: - 'config/environments/production.rb' - 'config/initializers/sidekiq.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + EnforcedStyle: percent + MinSize: 3 diff --git a/.ruby-version b/.ruby-version index 6cb9d3dd0d..9c25013dbb 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.3 +3.3.6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd4783597..02a12cfe5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,106 +2,6 @@ All notable changes to this project will be documented in this file. -## [4.3.7] - 2025-04-02 - -### Add - -- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire) -- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire) - -### Changed - -- Change account suspensions to be federated to recently-followed accounts as well (#34294 by @ClearlyClaire) -- Change `AccountReachFinder` to consider statuses based on suspension date (#32805 and #34291 by @ClearlyClaire and @mjankowski) -- Change user archive signed URL TTL from 10 seconds to 1 hour (#34254 by @ClearlyClaire) - -### Fixed - -- Fix static version of animated PNG emojis not being properly extracted (#34337 by @ClearlyClaire) -- Fix filters not applying in detailed view, favourites and bookmarks (#34259 and #34260 by @ClearlyClaire) -- Fix handling of malformed/unusual HTML (#34201 by @ClearlyClaire) -- Fix `CacheBuster` being queued for missing media attachments (#34253 by @ClearlyClaire) -- Fix incorrect URL being used when cache busting (#34189 by @ClearlyClaire) -- Fix streaming server refusing unix socket path in `DATABASE_URL` (#34091 by @ClearlyClaire) -- Fix “x” hotkey not working on boosted filtered posts (#33758 by @ClearlyClaire) - -## [4.3.6] - 2025-03-13 - -### Security - -- Update dependency `omniauth-saml` -- Update dependency `rack` - -### Fixed - -- Fix Stoplight errors when using `REDIS_NAMESPACE` (#34126 by @ClearlyClaire) - -## [4.3.5] - 2025-03-10 - -### Changed - -- Change hashtag suggestion to prefer personal history capitalization (#34070 by @ClearlyClaire) - -### Fixed - -- Fix processing errors for some HEIF images from iOS 18 (#34086 by @renchap) -- Fix streaming server not filtering unknown-language posts from public timelines (#33774 by @ClearlyClaire) -- Fix preview cards under Content Warnings not being shown in detailed statuses (#34068 by @ClearlyClaire) -- Fix username and display name being hidden on narrow screens in moderation interface (#33064 by @ClearlyClaire) - -## [4.3.4] - 2025-02-27 - -### Security - -- Update dependencies -- Change HTML sanitization to remove unusable and unused `embed` tag (#34021 by @ClearlyClaire, [GHSA-mq2m-hr29-8gqf](https://github.com/mastodon/mastodon/security/advisories/GHSA-mq2m-hr29-8gqf)) -- Fix rate-limit on sign-up email verification ([GHSA-v39f-c9jj-8w7h](https://github.com/mastodon/mastodon/security/advisories/GHSA-v39f-c9jj-8w7h)) -- Fix improper disclosure of domain blocks to unverified users ([GHSA-94h4-fj37-c825](https://github.com/mastodon/mastodon/security/advisories/GHSA-94h4-fj37-c825)) - -### Changed - -- Change preview cards to be shown when Content Warnings are expanded (#33827 by @ClearlyClaire) -- Change warnings against changing encryption secrets to be even more noticeable (#33631 by @ClearlyClaire) -- Change `mastodon:setup` to prevent overwriting already-configured servers (#33603, #33616, and #33684 by @ClearlyClaire and @mjankowski) -- Change notifications from moderators to not be filtered (#32974 and #33654 by @ClearlyClaire and @mjankowski) - -### Fixed - -- Fix `GET /api/v2/notifications/:id` and `POST /api/v2/notifications/:id/dismiss` for ungrouped notifications (#33990 by @ClearlyClaire) -- Fix issue with some versions of libvips on some systems (#33853 by @kleisauke) -- Fix handling of duplicate mentions in incoming status `Update` (#33911 by @ClearlyClaire) -- Fix inefficiencies in timeline generation (#33839 and #33842 by @ClearlyClaire) -- Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818 by @ClearlyClaire) -- Fix `tootctl feeds build` not building list timelines (#33783 by @ClearlyClaire) -- Fix flaky test in `/api/v2/notifications` tests (#33773 by @ClearlyClaire) -- Fix incorrect signature after HTTP redirect (#33757 and #33769 by @ClearlyClaire) -- Fix polls not being validated on edition (#33755 by @ClearlyClaire) -- Fix media preview height in compose form when 3 or more images are attached (#33571 by @ClearlyClaire) -- Fix preview card sizing in “Author attribution” in profile settings (#33482 by @ClearlyClaire) -- Fix processing of incoming notifications for unfilterable types (#33429 by @ClearlyClaire) -- Fix featured tags for remote accounts not being kept up to date (#33372, #33406, and #33425 by @ClearlyClaire and @mjankowski) -- Fix notification polling showing a loading bar in web UI (#32960 by @Gargron) -- Fix accounts table long display name (#29316 by @WebCoder49) -- Fix exclusive lists interfering with notifications (#28162 by @ShadowJonathan) - -## [4.3.3] - 2025-01-16 - -### Security - -- Fix insufficient validation of account URIs ([GHSA-5wxh-3p65-r4g6](https://github.com/mastodon/mastodon/security/advisories/GHSA-5wxh-3p65-r4g6)) -- Update dependencies - -### Fixed - -- Fix `libyaml` missing from `Dockerfile` build stage (#33591 by @vmstan) -- Fix incorrect notification settings migration for non-followers (#33348 by @ClearlyClaire) -- Fix down clause for notification policy v2 migrations (#33340 by @jesseplusplus) -- Fix error decrementing status count when `FeaturedTags#last_status_at` is `nil` (#33320 by @ClearlyClaire) -- Fix last paginated notification group only including data on a single notification (#33271 by @ClearlyClaire) -- Fix processing of mentions for post edits with an existing corresponding silent mention (#33227 by @ClearlyClaire) -- Fix deletion of unconfirmed users with Webauthn set (#33186 by @ClearlyClaire) -- Fix empty authors preview card serialization (#33151, #33466 by @mjankowski and @ClearlyClaire) - ## [4.3.2] - 2024-12-03 ### Added @@ -210,7 +110,7 @@ The following changelog entries focus on changes visible to users, administrator - `GET /api/v2/notifications`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped - `GET /api/v2/notifications/:group_key`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group - `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts - - `POST /api/v2/notifications/:group_key/dismiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group + - `POST /api/v2/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group - `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count - **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, #31541, #31723, #32062 and #32281 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\ The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\ @@ -235,7 +135,7 @@ The following changelog entries focus on changes visible to users, administrator - **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\ Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\ Note that this does not notify remote users.\ - This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event). + This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`relationship_severance_event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event). - **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\ Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\ This can be disabled in the “Animations and accessibility” section of the preferences. @@ -541,7 +441,7 @@ The following changelog entries focus on changes visible to users, administrator - Fix empty environment variables not using default nil value (#27400 by @renchap) - Fix language sorting in settings (#27158 by @gunchleoc) -## [4.2.11] - 2024-08-16 +## |4.2.11] - 2024-08-16 ### Added diff --git a/Dockerfile b/Dockerfile index 6620f4c096..4d6287912e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -# syntax=docker/dockerfile:1.12 +# syntax=docker/dockerfile:1.11 # This file is designed for production server deployment, not local development work -# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker +# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker # Please see https://docs.docker.com/engine/reference/builder for information about # the extended buildx capabilities used in this file. @@ -9,20 +9,19 @@ # See: https://docs.docker.com/build/building/multi-platform/ ARG TARGETPLATFORM=${TARGETPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM} -ARG BASE_REGISTRY="docker.io" -# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] +# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.4.2" -# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] +ARG RUBY_VERSION="3.3.6" +# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="22" # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] ARG DEBIAN_VERSION="bookworm" -# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim) -FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node -# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm) -FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby +# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) +FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node +# Ruby image to use for base image based on combined variables (ex: 3.3.x-slim-bookworm) +FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Example: v4.3.0-nightly.2023.11.09+pr-123456 @@ -61,7 +60,7 @@ ENV \ ENV \ # Configure the IP to bind Mastodon to when serving traffic BIND="0.0.0.0" \ - # Use production settings for Yarn, Node.js and related tools + # Use production settings for Yarn, Node and related nodejs based tools NODE_ENV="production" \ # Use production settings for Ruby on Rails RAILS_ENV="production" \ @@ -96,9 +95,6 @@ RUN \ # Set /opt/mastodon as working directory WORKDIR /opt/mastodon -# Add backport repository for some specific packages where we need the latest version -RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list - # hadolint ignore=DL3008,DL3005 RUN \ # Mount Apt cache and lib directories from Docker buildx caches @@ -128,6 +124,13 @@ RUN \ # Create temporary build layer from base image FROM ruby AS build +# Copy Node package configuration files into working directory +COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ +COPY .yarn /opt/mastodon/.yarn + +COPY --from=node /usr/local/bin /usr/local/bin +COPY --from=node /usr/local/lib /usr/local/lib + ARG TARGETPLATFORM # hadolint ignore=DL3008 @@ -150,7 +153,6 @@ RUN \ libpq-dev \ libssl-dev \ libtool \ - libyaml-dev \ meson \ nasm \ pkg-config \ @@ -161,7 +163,7 @@ RUN \ libexif-dev \ libexpat1-dev \ libgirepository1.0-dev \ - libheif-dev/bookworm-backports \ + libheif-dev \ libimagequant-dev \ libjpeg62-turbo-dev \ liblcms2-dev \ @@ -181,12 +183,18 @@ RUN \ libx265-dev \ ; +RUN \ + # Configure Corepack + rm /usr/local/bin/yarn*; \ + corepack enable; \ + corepack prepare --activate; + # Create temporary libvips specific build layer from build layer FROM build AS libvips # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips -ARG VIPS_VERSION=8.16.1 +ARG VIPS_VERSION=8.16.0 # libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] ARG VIPS_URL=https://github.com/libvips/libvips/releases/download @@ -271,37 +279,38 @@ RUN \ # Download and install required Gems bundle install -j"$(nproc)"; -# Create temporary assets build layer from build layer -FROM build AS precompiler +# Create temporary node specific build layer from build layer +FROM build AS yarn ARG TARGETPLATFORM -# Copy Mastodon sources into layer -COPY . /opt/mastodon/ - -# Copy Node.js binaries/libraries into layer -COPY --from=node /usr/local/bin /usr/local/bin -COPY --from=node /usr/local/lib /usr/local/lib - -RUN \ - # Configure Corepack - rm /usr/local/bin/yarn*; \ - corepack enable; \ - corepack prepare --activate; +# Copy Node package configuration files into working directory +COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ +COPY streaming/package.json /opt/mastodon/streaming/ +COPY .yarn /opt/mastodon/.yarn # hadolint ignore=DL3008 RUN \ --mount=type=cache,id=corepack-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/corepack,sharing=locked \ --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ - # Install Node.js packages + # Install Node packages yarn workspaces focus --production @mastodon/mastodon; -# Copy libvips components into layer for precompiler -COPY --from=libvips /usr/local/libvips/bin /usr/local/bin -COPY --from=libvips /usr/local/libvips/lib /usr/local/lib -# Copy bundler packages into layer for precompiler +# Create temporary assets build layer from build layer +FROM build AS precompiler + +# Copy Mastodon sources into precompiler layer +COPY . /opt/mastodon/ + +# Copy bundler and node packages from build layer to container +COPY --from=yarn /opt/mastodon /opt/mastodon/ COPY --from=bundler /opt/mastodon /opt/mastodon/ COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ +# Copy libvips components to layer for precompiler +COPY --from=libvips /usr/local/libvips/bin /usr/local/bin +COPY --from=libvips /usr/local/libvips/lib /usr/local/lib + +ARG TARGETPLATFORM RUN \ ldconfig; \ @@ -337,7 +346,7 @@ RUN \ # libvips components libcgif0 \ libexif12 \ - libheif1/bookworm-backports \ + libheif1 \ libimagequant0 \ libjpeg62-turbo \ liblcms2-2 \ diff --git a/Gemfile b/Gemfile index 9e5955e0b8..6abb075c1c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,12 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 3.2.0', '< 3.5.0' +ruby '>= 3.2.0' gem 'propshaft' gem 'puma', '~> 6.3' gem 'rack', '~> 2.2.7' -gem 'rails', '~> 8.0' +gem 'rails', '~> 7.2.0' gem 'thor', '~> 1.2' gem 'dotenv' @@ -14,7 +14,6 @@ gem 'haml-rails', '~>2.0' gem 'pg', '~> 1.5' gem 'pghero' -gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873 gem 'aws-sdk-s3', '~> 1.123', require: false gem 'blurhash', '~> 0.1' gem 'fog-core', '<= 2.6.0' @@ -40,7 +39,7 @@ gem 'net-ldap', '~> 0.18' gem 'omniauth', '~> 2.0' gem 'omniauth-cas', '~> 3.0.0.beta.1' -gem 'omniauth_openid_connect', '~> 0.8.0' +gem 'omniauth_openid_connect', '~> 0.6.1' gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-saml', '~> 2.0' @@ -62,7 +61,6 @@ gem 'inline_svg' gem 'irb', '~> 1.8' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' -gem 'linzer', '~> 0.6.1' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar' gem 'mutex_m' @@ -75,13 +73,13 @@ gem 'public_suffix', '~> 6.0' gem 'pundit', '~> 2.3' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' -gem 'rails-i18n', '~> 8.0' +gem 'rails-i18n', '~> 7.0' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'redis-namespace', '~> 1.10' gem 'rqrcode', '~> 2.2' gem 'ruby-progressbar', '~> 1.13' -gem 'sanitize', '~> 7.0' +gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' gem 'sidekiq-bulk', '~> 0.2.0' @@ -96,31 +94,29 @@ gem 'twitter-text', '~> 3.1.0' gem 'tzinfo-data', '~> 1.2023' gem 'webauthn', '~> 3.0' gem 'webpacker', '~> 5.4' -gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913' +gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -gem 'prometheus_exporter', '~> 2.2', require: false - -gem 'opentelemetry-api', '~> 1.5.0' +gem 'opentelemetry-api', '~> 1.4.0' group :opentelemetry do - gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false - gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false - gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false - gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false - gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false - gem 'opentelemetry-instrumentation-faraday', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-http', '~> 0.24.0', require: false - gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false - gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false - gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false - gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-rails', '~> 0.36.0', require: false - gem 'opentelemetry-instrumentation-redis', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-sidekiq', '~> 0.26.0', require: false + gem 'opentelemetry-exporter-otlp', '~> 0.29.0', require: false + gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false + gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false + gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false + gem 'opentelemetry-instrumentation-excon', '~> 0.22.0', require: false + gem 'opentelemetry-instrumentation-faraday', '~> 0.24.1', require: false + gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false + gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false + gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false + gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false + gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false + gem 'opentelemetry-instrumentation-rails', '~> 0.33.0', require: false + gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false + gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false gem 'opentelemetry-sdk', '~> 1.4', require: false end @@ -129,7 +125,7 @@ group :test do gem 'flatware-rspec' # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab - gem 'rspec-github', '~> 3.0', require: false + gem 'rspec-github', '~> 2.4', require: false # RSpec helpers for email specs gem 'email_spec' @@ -147,6 +143,9 @@ group :test do # Used to mock environment variables gem 'climate_control' + # Add back helpers functions removed in Rails 5.1 + gem 'rails-controller-testing', '~> 1.0' + # Validate schemas in specs gem 'json-schema', '~> 5.0' @@ -155,7 +154,7 @@ group :test do gem 'shoulda-matchers' - # Coverage formatter for RSpec + # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false gem 'simplecov-lcov', '~> 0.8', require: false @@ -167,14 +166,13 @@ group :development do # Code linting CLI and plugins gem 'rubocop', require: false gem 'rubocop-capybara', require: false - gem 'rubocop-i18n', require: false gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false gem 'rubocop-rspec', require: false gem 'rubocop-rspec_rails', require: false # Annotates modules with schema - gem 'annotaterb', '~> 4.13', require: false + gem 'annotaterb', '~> 4.13' # Enhanced error message pages for development gem 'better_errors', '~> 2.9' @@ -185,7 +183,7 @@ group :development do gem 'letter_opener_web', '~> 3.0' # Security analysis CLI tools - gem 'brakeman', '~> 7.0', require: false + gem 'brakeman', '~> 6.0', require: false gem 'bundler-audit', '~> 0.9', require: false # Linter CLI for HAML files @@ -197,7 +195,7 @@ end group :development, :test do # Interactive Debugging tools - gem 'debug', '~> 1.8', require: false + gem 'debug', '~> 1.8' # Generate fake data values gem 'faker', '~> 3.2' @@ -209,7 +207,7 @@ group :development, :test do gem 'memory_profiler', require: false gem 'ruby-prof', require: false gem 'stackprof', require: false - gem 'test-prof', require: false + gem 'test-prof' # RSpec runner for rails gem 'rspec-rails', '~> 7.0' @@ -224,7 +222,7 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'net-http', '~> 0.6.0' +gem 'net-http', '~> 0.5.0' gem 'rubyzip', '~> 2.3' gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index f13df0c43f..3bbfb33d74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,79 +1,80 @@ GIT - remote: https://github.com/mastodon/webpush.git - revision: 9631ac63045cfabddacc69fc06e919b4c13eb913 - ref: 9631ac63045cfabddacc69fc06e919b4c13eb913 + remote: https://github.com/ClearlyClaire/webpush.git + revision: f14a4d52e201128b1b00245d11b6de80d6cfdcd9 + ref: f14a4d52e201128b1b00245d11b6de80d6cfdcd9 specs: - webpush (1.1.0) + webpush (0.3.8) hkdf (~> 0.2) jwt (~> 2.0) GEM remote: https://rubygems.org/ specs: - actioncable (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + actioncable (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + actionmailbox (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) mail (>= 2.8.0) - actionmailer (8.0.2) - actionpack (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activesupport (= 8.0.2) + actionmailer (7.2.2) + actionpack (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activesupport (= 7.2.2) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.2) - actionview (= 8.0.2) - activesupport (= 8.0.2) + actionpack (7.2.2) + actionview (= 7.2.2) + activesupport (= 7.2.2) nokogiri (>= 1.8.5) - rack (>= 2.2.4) + racc + rack (>= 2.2.4, < 3.2) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.2) - actionpack (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + actiontext (7.2.2) + actionpack (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.2) - activesupport (= 8.0.2) + actionview (7.2.2) + activesupport (= 7.2.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - active_model_serializers (0.10.15) + active_model_serializers (0.10.14) actionpack (>= 4.1) activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (8.0.2) - activesupport (= 8.0.2) + activejob (7.2.2) + activesupport (= 7.2.2) globalid (>= 0.3.6) - activemodel (8.0.2) - activesupport (= 8.0.2) - activerecord (8.0.2) - activemodel (= 8.0.2) - activesupport (= 8.0.2) + activemodel (7.2.2) + activesupport (= 7.2.2) + activerecord (7.2.2) + activemodel (= 7.2.2) + activesupport (= 7.2.2) timeout (>= 0.4.0) - activestorage (8.0.2) - actionpack (= 8.0.2) - activejob (= 8.0.2) - activerecord (= 8.0.2) - activesupport (= 8.0.2) + activestorage (7.2.2) + actionpack (= 7.2.2) + activejob (= 7.2.2) + activerecord (= 7.2.2) + activesupport (= 7.2.2) marcel (~> 1.0) - activesupport (8.0.2) + activesupport (7.2.2) base64 benchmark (>= 0.3) bigdecimal @@ -85,17 +86,16 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) android_key_attestation (0.3.0) - annotaterb (4.14.0) - ast (2.4.3) + annotaterb (4.13.0) + ast (2.4.2) attr_required (1.0.2) - aws-eventstream (1.3.2) - aws-partitions (1.1087.0) - aws-sdk-core (3.215.1) + aws-eventstream (1.3.0) + aws-partitions (1.1013.0) + aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -103,13 +103,13 @@ GEM aws-sdk-kms (1.96.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.177.0) + aws-sdk-s3 (1.174.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) - azure-blob (0.5.7) + azure-blob (0.5.3) rexml base64 (0.2.0) bcp47_spec (0.2.1) @@ -119,16 +119,16 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.9) - bindata (2.5.1) + bigdecimal (3.1.8) + bindata (2.5.0) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) blurhash (0.1.8) bootsnap (1.18.4) msgpack (~> 1.2) - brakeman (7.0.2) + brakeman (6.2.2) racc - browser (6.2.0) + browser (6.1.0) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) @@ -159,8 +159,8 @@ GEM climate_control (1.2.0) cocoon (1.2.15) color_diff (0.1) - concurrent-ruby (1.3.5) - connection_pool (2.5.0) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) cose (1.3.1) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -168,15 +168,15 @@ GEM bigdecimal rexml crass (1.0.6) - css_parser (1.21.1) + css_parser (1.19.1) addressable - csv (3.3.4) + csv (3.3.0) database_cleaner-active_record (2.2.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.4.1) - debug (1.10.0) + date (3.4.0) + debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) debug_inspector (1.2.0) @@ -194,14 +194,14 @@ GEM devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) rpam2 (~> 4.0) - diff-lcs (1.6.1) + diff-lcs (1.5.1) discard (1.4.0) activerecord (>= 4.2, < 9.0) docile (1.4.1) domain_name (0.6.20240107) - doorkeeper (5.8.2) + doorkeeper (5.8.0) railties (>= 5) - dotenv (3.1.8) + dotenv (3.1.4) drb (2.2.1) elasticsearch (7.17.11) elasticsearch-api (= 7.17.11) @@ -217,47 +217,42 @@ GEM htmlentities (~> 4.3.3) launchy (>= 2.1, < 4.0) mail (~> 2.7) - email_validator (2.2.4) - activemodel - erubi (1.13.1) + erubi (1.13.0) et-orbi (1.2.11) tzinfo - excon (1.2.5) - logger + excon (0.112.0) fabrication (2.31.0) faker (3.5.1) i18n (>= 1.8.11, < 2) - faraday (2.13.0) - faraday-net_http (>= 2.0, < 3.5) + faraday (2.12.0) + faraday-net_http (>= 2.0, < 3.4) json logger - faraday-follow_redirects (0.3.0) - faraday (>= 1, < 3) faraday-httpclient (2.0.1) httpclient (>= 2.2) - faraday-net_http (3.4.0) - net-http (>= 0.5.0) + faraday-net_http (3.3.0) + net-http fast_blank (1.0.1) - fastimage (2.4.0) - ffi (1.17.2) + fastimage (2.3.1) + ffi (1.17.0) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake - flatware (2.3.4) + flatware (2.3.3) drb thor (< 2.0) - flatware-rspec (2.3.4) - flatware (= 2.3.4) + flatware-rspec (2.3.3) + flatware (= 2.3.3) rspec (>= 3.6) - fog-core (2.6.0) + fog-core (2.5.0) builder - excon (~> 1.0) + excon (~> 0.71) formatador (>= 0.2, < 2.0) mime-types fog-json (1.2.0) fog-core multi_json (~> 1.10) - fog-openstack (1.1.5) + fog-openstack (1.1.3) fog-core (~> 2.1) fog-json (>= 1.0) formatador (1.1.0) @@ -266,10 +261,8 @@ GEM raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (4.30.2) - bigdecimal - rake (>= 13) - googleapis-common-protos-types (1.19.0) + google-protobuf (3.25.5) + googleapis-common-protos-types (1.15.0) google-protobuf (>= 3.18, < 5.a) haml (6.3.0) temple (>= 0.8.2) @@ -280,17 +273,17 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.62.0) + haml_lint (0.59.0) haml (>= 5.0) parallel (~> 1.10) rainbow rubocop (>= 1.0) sysexits (~> 1.1) - hashdiff (1.1.2) + hashdiff (1.1.1) hashie (5.0.0) hcaptcha (7.1.0) json - highline (3.1.2) + highline (3.1.1) reline hiredis (0.6.3) hkdf (0.3.0) @@ -301,18 +294,17 @@ GEM http-cookie (~> 1.0) http-form_data (~> 2.2) llhttp-ffi (~> 0.5.0) - http-cookie (1.0.8) + http-cookie (1.0.5) domain_name (~> 0.5) http-form_data (2.3.0) http_accept_language (2.1.1) - httpclient (2.9.0) - mutex_m + httpclient (2.8.3) httplog (1.7.0) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.14.7) + i18n (1.14.6) concurrent-ruby (~> 1.0) - i18n-tasks (1.0.15) + i18n-tasks (1.0.14) activesupport (>= 4.0.2) ast (>= 2.1.0) erubi @@ -321,15 +313,13 @@ GEM parser (>= 3.2.2.1) rails-i18n rainbow (>= 2.2.2, < 4.0) - ruby-progressbar (~> 1.8, >= 1.8.1) terminal-table (>= 1.5.1) idn-ruby (0.1.5) inline_svg (1.10.0) activesupport (>= 3.0) nokogiri (>= 1.6) - io-console (0.8.0) - irb (1.15.2) - pp (>= 0.6.0) + io-console (0.7.2) + irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) jd-paperclip-azure (3.0.0) @@ -337,15 +327,13 @@ GEM azure-blob (~> 0.5.2) hashie (~> 5.0) jmespath (1.6.2) - json (2.10.2) + json (2.8.1) json-canonicalization (1.0.0) - json-jwt (1.16.7) + json-jwt (1.15.3.1) activesupport (>= 4.2) aes_key_wrap - base64 bindata - faraday (~> 2.0) - faraday-follow_redirects + httpclient json-ld (3.3.2) htmlentities (~> 4.3) json-canonicalization (~> 1.0) @@ -357,11 +345,10 @@ GEM json-ld-preloaded (3.3.1) json-ld (~> 3.3) rdf (~> 3.3) - json-schema (5.1.1) + json-schema (5.1.0) addressable (~> 2.8) - bigdecimal (~> 3.1) jsonapi-renderer (0.2.2) - jwt (2.10.1) + jwt (2.9.3) base64 kaminari (1.2.2) activesupport (>= 4.1.0) @@ -381,11 +368,10 @@ GEM marcel (~> 1.0.1) mime-types terrapin (>= 0.6.0, < 2.0) - language_server-protocol (3.17.0.4) - launchy (3.1.1) + language_server-protocol (3.17.0.3) + launchy (3.0.1) addressable (~> 2.8) childprocess (~> 5.0) - logger (~> 1.6) letter_opener (1.10.0) launchy (>= 2.2, < 4) letter_opener_web (3.0.0) @@ -394,23 +380,16 @@ GEM railties (>= 6.1) rexml link_header (0.0.8) - lint_roller (1.1.0) - linzer (0.6.5) - openssl (~> 3.0, >= 3.0.0) - rack (>= 2.2, < 4.0) - starry (~> 0.2) - stringio (~> 3.1, >= 3.1.2) - uri (~> 1.0, >= 1.0.2) - llhttp-ffi (0.5.1) + llhttp-ffi (0.5.0) ffi-compiler (~> 1.0) rake (~> 13.0) - logger (1.7.0) + logger (1.6.1) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.24.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -423,19 +402,19 @@ GEM redis (>= 3.0.5) matrix (0.4.2) memory_profiler (1.1.0) - mime-types (3.6.2) + mime-types (3.6.0) logger mime-types-data (~> 3.2015) - mime-types-data (3.2025.0408) + mime-types-data (3.2024.1105) mini_mime (1.1.5) - mini_portile2 (2.8.8) - minitest (5.25.5) - msgpack (1.8.0) + mini_portile2 (2.8.7) + minitest (5.25.1) + msgpack (1.7.5) multi_json (1.15.0) mutex_m (0.3.0) - net-http (0.6.0) + net-http (0.5.0) uri - net-imap (0.5.6) + net-imap (0.5.1) date net-protocol net-ldap (0.19.0) @@ -443,160 +422,148 @@ GEM net-protocol net-protocol (0.2.2) timeout - net-smtp (0.5.1) + net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.18.7) + nokogiri (1.16.7) mini_portile2 (~> 2.8.2) racc (~> 1.4) - oj (3.16.10) + oj (3.16.7) bigdecimal (>= 3.0) ostruct (>= 0.2) - omniauth (2.1.3) + omniauth (2.1.2) hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection - omniauth-cas (3.0.1) + omniauth-cas (3.0.0) addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.2.3) + omniauth-saml (2.2.1) omniauth (~> 2.1) - ruby-saml (~> 1.18) - omniauth_openid_connect (0.8.0) + ruby-saml (~> 1.17) + omniauth_openid_connect (0.6.1) omniauth (>= 1.9, < 3) - openid_connect (~> 2.2) - openid_connect (2.3.1) + openid_connect (~> 1.1) + openid_connect (1.4.2) activemodel attr_required (>= 1.0.0) - email_validator - faraday (~> 2.0) - faraday-follow_redirects - json-jwt (>= 1.16) - mail - rack-oauth2 (~> 2.2) - swd (~> 2.0) + json-jwt (>= 1.15.0) + net-smtp + rack-oauth2 (~> 1.21) + swd (~> 1.3) tzinfo + validate_email validate_url - webfinger (~> 2.0) - openssl (3.3.0) + webfinger (~> 1.2) + openssl (3.2.0) openssl-signature_algorithm (1.3.0) openssl (> 2.0) - opentelemetry-api (1.5.0) - opentelemetry-common (0.22.0) + opentelemetry-api (1.4.0) + opentelemetry-common (0.21.0) opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.30.0) + opentelemetry-exporter-otlp (0.29.0) google-protobuf (>= 3.18) googleapis-common-protos-types (~> 1.3) opentelemetry-api (~> 1.1) opentelemetry-common (~> 0.20) opentelemetry-sdk (~> 1.2) opentelemetry-semantic_conventions - opentelemetry-helpers-sql-obfuscation (0.3.0) + opentelemetry-helpers-sql-obfuscation (0.2.0) opentelemetry-common (~> 0.21) - opentelemetry-instrumentation-action_mailer (0.4.0) + opentelemetry-instrumentation-action_mailer (0.2.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.7) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-action_pack (0.12.0) + opentelemetry-instrumentation-active_support (~> 0.1) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-action_pack (0.10.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) + opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-rack (~> 0.21) - opentelemetry-instrumentation-action_view (0.9.0) + opentelemetry-instrumentation-action_view (0.7.3) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.7) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_job (0.8.0) + opentelemetry-instrumentation-active_support (~> 0.6) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-active_job (0.7.8) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_model_serializers (0.22.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-active_model_serializers (0.20.2) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (>= 0.7.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_record (0.9.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-active_record (0.8.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_storage (0.1.1) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-active_support (0.6.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-active_support (~> 0.7) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-active_support (0.8.0) - opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-base (0.23.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-base (0.22.6) opentelemetry-api (~> 1.0) opentelemetry-common (~> 0.21) opentelemetry-registry (~> 0.1) - opentelemetry-instrumentation-concurrent_ruby (0.22.0) + opentelemetry-instrumentation-concurrent_ruby (0.21.4) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-excon (0.23.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-excon (0.22.4) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-faraday (0.26.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-faraday (0.24.6) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-http (0.24.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-http (0.23.4) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-http_client (0.23.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-http_client (0.22.7) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-net_http (0.23.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-net_http (0.22.7) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-pg (0.30.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-pg (0.29.0) opentelemetry-api (~> 1.0) opentelemetry-helpers-sql-obfuscation - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-rack (0.26.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rack (0.25.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-rails (0.36.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-rails (0.33.0) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-action_mailer (~> 0.4.0) - opentelemetry-instrumentation-action_pack (~> 0.12.0) - opentelemetry-instrumentation-action_view (~> 0.9.0) - opentelemetry-instrumentation-active_job (~> 0.8.0) - opentelemetry-instrumentation-active_record (~> 0.9.0) - opentelemetry-instrumentation-active_storage (~> 0.1.0) - opentelemetry-instrumentation-active_support (~> 0.8.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0) - opentelemetry-instrumentation-redis (0.26.1) + opentelemetry-instrumentation-action_mailer (~> 0.2.0) + opentelemetry-instrumentation-action_pack (~> 0.10.0) + opentelemetry-instrumentation-action_view (~> 0.7.0) + opentelemetry-instrumentation-active_job (~> 0.7.0) + opentelemetry-instrumentation-active_record (~> 0.8.0) + opentelemetry-instrumentation-active_support (~> 0.6.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-redis (0.25.7) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-sidekiq (0.26.1) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-instrumentation-sidekiq (0.25.7) opentelemetry-api (~> 1.0) - opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-registry (0.4.0) + opentelemetry-instrumentation-base (~> 0.22.1) + opentelemetry-registry (0.3.1) opentelemetry-api (~> 1.1) - opentelemetry-sdk (1.8.0) + opentelemetry-sdk (1.5.0) opentelemetry-api (~> 1.1) opentelemetry-common (~> 0.20) opentelemetry-registry (~> 0.2) opentelemetry-semantic_conventions - opentelemetry-semantic_conventions (1.11.0) + opentelemetry-semantic_conventions (1.10.1) opentelemetry-api (~> 1.0) orm_adapter (0.5.0) ostruct (0.6.1) - ox (2.14.22) - bigdecimal (>= 3.0) - parallel (1.27.0) - parser (3.3.8.0) + ox (2.14.18) + parallel (1.26.3) + parser (3.3.6.0) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) pg (1.5.9) - pghero (3.6.2) + pghero (3.6.1) activerecord (>= 6.1) - pp (0.6.2) - prettyprint premailer (1.27.0) addressable css_parser (>= 1.19.0) @@ -605,35 +572,29 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) - prettyprint (0.2.0) - prism (1.4.0) - prometheus_exporter (2.2.0) - webrick propshaft (1.1.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) rack railties (>= 7.0.0) - psych (5.2.3) - date + psych (5.2.0) stringio public_suffix (6.0.1) - puma (6.6.0) + puma (6.5.0) nio4r (~> 2.0) - pundit (2.5.0) + pundit (2.4.0) activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.13) + rack (2.2.10) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) rack (>= 2.0.0) - rack-oauth2 (2.2.1) + rack-oauth2 (1.21.3) activesupport attr_required - faraday (~> 2.0) - faraday-follow_redirects + httpclient json-jwt (>= 1.11.0) rack (>= 2.1.0) rack-protection (3.2.0) @@ -643,38 +604,42 @@ GEM rack rack-session (1.0.2) rack (< 3) - rack-test (2.2.0) + rack-test (2.1.0) rack (>= 1.3) - rackup (1.0.1) + rackup (1.0.0) rack (< 3) webrick - rails (8.0.2) - actioncable (= 8.0.2) - actionmailbox (= 8.0.2) - actionmailer (= 8.0.2) - actionpack (= 8.0.2) - actiontext (= 8.0.2) - actionview (= 8.0.2) - activejob (= 8.0.2) - activemodel (= 8.0.2) - activerecord (= 8.0.2) - activestorage (= 8.0.2) - activesupport (= 8.0.2) + rails (7.2.2) + actioncable (= 7.2.2) + actionmailbox (= 7.2.2) + actionmailer (= 7.2.2) + actionpack (= 7.2.2) + actiontext (= 7.2.2) + actionview (= 7.2.2) + activejob (= 7.2.2) + activemodel (= 7.2.2) + activerecord (= 7.2.2) + activestorage (= 7.2.2) + activesupport (= 7.2.2) bundler (>= 1.15.0) - railties (= 8.0.2) + railties (= 7.2.2) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) + rails-html-sanitizer (1.6.0) loofah (~> 2.21) - nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - rails-i18n (8.0.1) + nokogiri (~> 1.14) + rails-i18n (7.0.10) i18n (>= 0.7, < 2) - railties (>= 8.0.0, < 9) - railties (8.0.2) - actionpack (= 8.0.2) - activesupport (= 8.0.2) + railties (>= 6.0.0, < 8) + railties (7.2.2) + actionpack (= 7.2.2) + activesupport (= 7.2.2) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -688,23 +653,23 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (6.13.1) + rdoc (6.7.0) psych (>= 4.0.0) - redcarpet (3.6.1) + redcarpet (3.6.0) redis (4.8.1) redis-namespace (1.11.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.10.0) - reline (0.6.1) + regexp_parser (2.9.2) + reline (0.5.11) io-console (~> 0.5) - request_store (1.7.0) + request_store (1.6.0) rack (>= 1.4) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.4.1) + rexml (3.3.9) rotp (6.3.0) rouge (4.5.1) rpam2 (4.0.2) @@ -716,17 +681,17 @@ GEM rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.3) + rspec-core (3.13.2) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-github (3.0.0) + rspec-github (2.4.0) rspec-core (~> 3.0) rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (7.1.1) + rspec-rails (7.1.0) actionpack (>= 7.0) activesupport (>= 7.0) railties (>= 7.0) @@ -734,76 +699,66 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-sidekiq (5.1.0) + rspec-sidekiq (5.0.0) rspec-core (~> 3.0) rspec-expectations (~> 3.0) rspec-mocks (~> 3.0) - sidekiq (>= 5, < 9) - rspec-support (3.13.2) - rubocop (1.75.2) + sidekiq (>= 5, < 8) + rspec-support (3.13.1) + rubocop (1.66.1) json (~> 2.3) - language_server-protocol (~> 3.17.0.2) - lint_roller (~> 1.1.0) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.44.0, < 2.0) + regexp_parser (>= 2.4, < 3.0) + rubocop-ast (>= 1.32.2, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.44.1) - parser (>= 3.3.7.2) - prism (~> 1.4) - rubocop-capybara (2.22.1) - lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) - rubocop-i18n (3.2.3) - lint_roller (~> 1.1) - rubocop (>= 1.72.1) - rubocop-performance (1.25.0) - lint_roller (~> 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rails (2.31.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.32.3) + parser (>= 3.3.1.0) + rubocop-capybara (2.21.0) + rubocop (~> 1.41) + rubocop-performance (1.22.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.27.0) activesupport (>= 4.2.0) - lint_roller (~> 1.1) rack (>= 1.1) - rubocop (>= 1.75.0, < 2.0) - rubocop-ast (>= 1.38.0, < 2.0) - rubocop-rspec (3.5.0) - lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) - rubocop-rspec_rails (2.31.0) - lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) - rubocop-rspec (~> 3.5) + rubocop (>= 1.52.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rspec (3.2.0) + rubocop (~> 1.61) + rubocop-rspec_rails (2.30.0) + rubocop (~> 1.61) + rubocop-rspec (~> 3, >= 3.0.1) ruby-prof (1.7.1) ruby-progressbar (1.13.0) - ruby-saml (1.18.0) + ruby-saml (1.17.0) nokogiri (>= 1.13.10) rexml - ruby-vips (2.2.3) + ruby-vips (2.2.2) ffi (~> 1.12) logger - rubyzip (2.4.1) - rufus-scheduler (3.9.2) - fugit (~> 1.1, >= 1.11.1) + rubyzip (2.3.2) + rufus-scheduler (3.9.1) + fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) - sanitize (7.0.0) + sanitize (6.1.3) crass (~> 1.0.2) - nokogiri (>= 1.16.8) + nokogiri (>= 1.12.0) scenic (1.8.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - securerandom (0.4.1) - selenium-webdriver (4.31.0) + securerandom (0.3.2) + selenium-webdriver (4.27.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - semantic_range (3.1.0) + semantic_range (3.0.0) shoulda-matchers (6.4.0) activesupport (>= 5.2.0) sidekiq (6.5.12) @@ -834,30 +789,27 @@ GEM simplecov-html (0.13.1) simplecov-lcov (0.8.0) simplecov_json_formatter (0.1.4) - stackprof (0.2.27) - starry (0.2.0) - base64 - stoplight (4.1.1) + stackprof (0.2.26) + stoplight (4.1.0) redlock (~> 1.0) - stringio (3.1.6) - strong_migrations (2.3.0) - activerecord (>= 7) - swd (2.0.3) + stringio (3.1.2) + strong_migrations (2.1.0) + activerecord (>= 6.1) + swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) - faraday (~> 2.0) - faraday-follow_redirects + httpclient (>= 2.4) sysexits (1.2.0) temple (0.10.3) - terminal-table (4.0.0) - unicode-display_width (>= 1.1.1, < 4) - terrapin (1.1.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + terrapin (1.0.1) climate_control - test-prof (1.4.4) + test-prof (1.4.2) thor (1.3.2) - tilt (2.6.0) - timeout (0.4.3) - tpm-key_attestation (0.14.0) + tilt (2.4.0) + timeout (0.4.2) + tpm-key_attestation (0.12.1) bindata (~> 2.4) openssl (> 2.0) openssl-signature_algorithm (~> 1.0) @@ -876,34 +828,34 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2025.2) + tzinfo-data (1.2024.2) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.9.1) - unicode-display_width (3.1.4) - unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) - uri (1.0.3) - useragent (0.16.11) + unicode-display_width (2.6.0) + uri (0.13.1) + useragent (0.16.10) + validate_email (0.1.6) + activemodel (>= 3.0) + mail (>= 2.2.5) validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix warden (1.2.9) rack (>= 2.0.9) - webauthn (3.4.0) + webauthn (3.2.2) android_key_attestation (~> 0.3.0) bindata (~> 2.4) cbor (~> 0.5.9) cose (~> 1.1) openssl (>= 2.2) safety_net_attestation (~> 0.4.0) - tpm-key_attestation (~> 0.14.0) - webfinger (2.1.3) + tpm-key_attestation (~> 0.12.0) + webfinger (1.2.0) activesupport - faraday (~> 2.0) - faraday-follow_redirects - webmock (3.25.1) + httpclient (>= 2.4) + webmock (3.24.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -912,17 +864,16 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - webrick (1.9.1) + webrick (1.9.0) websocket (1.2.11) - websocket-driver (0.7.7) - base64 + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.1) xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.7.1) PLATFORMS ruby @@ -931,13 +882,12 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotaterb (~> 4.13) - aws-sdk-core (< 3.216.0) aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) bootsnap (~> 1.18.0) - brakeman (~> 7.0) + brakeman (~> 6.0) browser bundler-audit (~> 0.9) capybara (~> 3.39) @@ -988,14 +938,13 @@ DEPENDENCIES letter_opener (~> 1.8) letter_opener_web (~> 3.0) link_header (~> 0.0) - linzer (~> 0.6.1) lograge (~> 0.12) mail (~> 2.8) mario-redis-lock (~> 1.2) memory_profiler mime-types (~> 3.6.0) mutex_m - net-http (~> 0.6.0) + net-http (~> 0.5.0) net-ldap (~> 0.18) nokogiri (~> 1.15) oj (~> 3.14) @@ -1003,29 +952,28 @@ DEPENDENCIES omniauth-cas (~> 3.0.0.beta.1) omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.0) - omniauth_openid_connect (~> 0.8.0) - opentelemetry-api (~> 1.5.0) - opentelemetry-exporter-otlp (~> 0.30.0) - opentelemetry-instrumentation-active_job (~> 0.8.0) - opentelemetry-instrumentation-active_model_serializers (~> 0.22.0) - opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0) - opentelemetry-instrumentation-excon (~> 0.23.0) - opentelemetry-instrumentation-faraday (~> 0.26.0) - opentelemetry-instrumentation-http (~> 0.24.0) - opentelemetry-instrumentation-http_client (~> 0.23.0) - opentelemetry-instrumentation-net_http (~> 0.23.0) - opentelemetry-instrumentation-pg (~> 0.30.0) - opentelemetry-instrumentation-rack (~> 0.26.0) - opentelemetry-instrumentation-rails (~> 0.36.0) - opentelemetry-instrumentation-redis (~> 0.26.0) - opentelemetry-instrumentation-sidekiq (~> 0.26.0) + omniauth_openid_connect (~> 0.6.1) + opentelemetry-api (~> 1.4.0) + opentelemetry-exporter-otlp (~> 0.29.0) + opentelemetry-instrumentation-active_job (~> 0.7.1) + opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) + opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) + opentelemetry-instrumentation-excon (~> 0.22.0) + opentelemetry-instrumentation-faraday (~> 0.24.1) + opentelemetry-instrumentation-http (~> 0.23.2) + opentelemetry-instrumentation-http_client (~> 0.22.3) + opentelemetry-instrumentation-net_http (~> 0.22.4) + opentelemetry-instrumentation-pg (~> 0.29.0) + opentelemetry-instrumentation-rack (~> 0.25.0) + opentelemetry-instrumentation-rails (~> 0.33.0) + opentelemetry-instrumentation-redis (~> 0.25.3) + opentelemetry-instrumentation-sidekiq (~> 0.25.2) opentelemetry-sdk (~> 1.4) ox (~> 2.14) parslet pg (~> 1.5) pghero premailer-rails - prometheus_exporter (~> 2.2) propshaft public_suffix (~> 6.0) puma (~> 6.3) @@ -1034,19 +982,19 @@ DEPENDENCIES rack-attack (~> 6.6) rack-cors (~> 2.0) rack-test (~> 2.1) - rails (~> 8.0) - rails-i18n (~> 8.0) + rails (~> 7.2.0) + rails-controller-testing (~> 1.0) + rails-i18n (~> 7.0) rdf-normalize (~> 0.5) redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) rqrcode (~> 2.2) - rspec-github (~> 3.0) + rspec-github (~> 2.4) rspec-rails (~> 7.0) rspec-sidekiq (~> 5.0) rubocop rubocop-capybara - rubocop-i18n rubocop-performance rubocop-rails rubocop-rspec @@ -1055,7 +1003,7 @@ DEPENDENCIES ruby-progressbar (~> 1.13) ruby-vips (~> 2.2) rubyzip (~> 2.3) - sanitize (~> 7.0) + sanitize (~> 6.0) scenic (~> 1.7) selenium-webdriver shoulda-matchers @@ -1082,7 +1030,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.4.1p0 + ruby 3.3.6p108 BUNDLED WITH - 2.6.8 + 2.5.23 diff --git a/README.md b/README.md index 854e8ac3d9..200d58d8c4 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,123 @@ -NAS is an KMY & Mastodon Fork +# ![kmyblue icon](https://raw.githubusercontent.com/kmycode/mastodon/kb_development/app/javascript/icons/favicon-32x32.png) kmyblue -The following are just a few of the most common features. There are many other minor changes to the specifications. +[![Ruby Testing](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml/badge.svg)](https://github.com/kmycode/mastodon/actions/workflows/test-ruby.yml) -Emoji reactions +! FOR ENGLISH USER ! We do not provide English documentation for kmyblue; we assume that you will use automatic translation software, such as Google, to translate the site. -Local Public (Does not appear on the federated timeline of remote servers, but does appear on followers' home timelines. This is different from local only) +kmyblueは、ActivityPubに接続するSNSの1つである[Mastodon](https://github.com/mastodon/mastodon)のフォークです。創作作家のためのMastodonを目指して開発しました。 -Bookmark classification +kmyblueはフォーク名であり、同時に[サーバー名](https://kmy.blue)でもあります。以下は特に記述がない限り、フォークとしてのkmyblueをさします。 -Set who can search your posts for each post (Searchability) +kmyblueは AGPL ライセンスで公開されているため、どなたでも自由にフォークし、このソースコードを元に自分でサーバーを立てて公開することができます。確かにサーバーkmyblueは創作作家向けの利用規約が設定されていますが、フォークとしてのkmyblueのルールは全くの別物です。いかなるコミュニティにも平等にお使いいただけます。 +kmyblueは、閉鎖的なコミュニティ、あまり目立ちたくないコミュニティには特に強力な機能を提供します。kmyblueはプライバシーを考慮したうえで強力な独自機能を提供するため、汎用サーバーとして利用するにもある程度十分な機能が揃っています。 -Quote posts, modest quotes (references) +テストコード、Lint どちらも動いています。 -Record posts that meet certain conditions such as domains, accounts, and keywords (Subscriptions/Antennas) +### アジェンダ -Send posts to a designated set of followers (Circles) (different from direct messages) +- 利用方法 +- kmyblueの開発方針 +- kmyblueは何でないか +- kmyblueの独自機能 +- 英語のサポートについて -Notification of new posts on lists +## 利用方法 -Exclude posts from people you follow when filtering posts +### インストール方法 -Hide number of followers and followings +[Wiki](https://github.com/kmycode/mastodon/wiki/Installation)を参照してください。 -Automatically delete posts after a specified time has passed +### 開発への参加方法 -Expanding moderation functions +CONTRIBUTING.mdを参照してください。 + +### テスト + +``` +# デバッグ実行(以下のいずれか) +foreman start +DB_USER=postgres DB_PASS=password foreman start + +# 一部を除く全てのテストを行う +RAILS_ENV=test bundle exec rspec spec + +# ElasticSearch連携テストを行う +新 +RAILS_ENV=test ES_ENABLED=true bundle exec rspec --tag search +旧 +RAILS_ENV=test ES_ENABLED=true RUN_SEARCH_SPECS=true bundle exec rspec spec/search +``` + +## kmyblueの開発方針 + +### 本家Mastodonへの積極的追従 + +kmyblueは、追加機能を控えめにする代わりに本家Mastodonに積極的に追従を行います。kmyblueの追加機能そのままに、Mastodonの新機能も利用できるよう調整を行います。 + +### ゆるやかな内輪での運用 + +kmyblueは同人向けサーバーとして出発したため、同人作家に需要のある「内輪ノリを外部にできるだけもらさない」という部分に特化しています。 + +「ローカル公開」は、投稿を見せたくない人に見つかりにくくする効果があります。「サークル」は、フォロワーの中でも特に見せたい人だけに見せる効果があります。 +「検索許可」という独自の検索オプションを利用することで、公開投稿の一部だけを検索されにくくするだけでなく、非収載投稿が誰でも自由に検索できるようになります。 + +内輪とは自分のサーバーに限ったものではありません。内輪同士で複数のサーバーを運営するとき、お互いが深く繋がれる「フレンドサーバー」というシステムも用意しています。 + +### 少人数サーバーでの運用 + +kmyblueは、人の少ないサーバーでの運用を考慮して設計しています。そのため、Fedibirdにあるような、人の多いサーバー向けの機能はあまり作っていません。 + +サーバーの負荷については一部度外視している部分があります。たとえば絵文字リアクション機能はサーバーへ著しい負荷をかける場合があります。ただしkmyblueでは、絵文字リアクション機能そのものを無効にしたり、負荷の高いストリーミング処理を無効にする管理者オプションも存在します。 + +もちろん人の多いサーバーでの運用が不便になるような修正は行っていません。人数にかかわらず、そのままお使いいただけます。 + +### 比較的高い防御力 + +kmyblueでは、「Fediverseは将来的に荒むのではないか」「Fediverseは将来的にスパムに溢れるのではないか」を念頭に設計している部分があります。投稿だけでなく絵文字リアクションも対象にした防衛策があります。 + +管理者は「NGワード」「NGルール」機能の利用が可能です。設定を変更することで、一部のモデレーターもこの機能を利用できます。 +利用者は、独自拡張されたフィルター機能、絵文字リアクションのブロックなどを利用できます。 + +ただし防御力の高さは自由を犠牲にします。例えばNGワードが多すぎると、他のサーバーからの投稿が制限され、かつそれに気づきにくくなります。 + +## kmyblueは何でないか + +kmyblueは、企業・政府機関向けに開発されたものではありません。開発者はセキュリティに関する専門知識を有しておらず、高度なセキュリティを求められる機関向けのソフトウェアを制作する能力はありません。また、kmyblueのメンテナは現在1人のみであり、そのメンテナが飽きたら開発がストップするリスクも高いです。Mastodonのような高い信頼性・安全性を保証することはできないので、導入の際はご自身で安全を十分に確認してからお使いになることを強くおすすめします。 +個人サーバーであっても、安定性を強く求める方にはおすすめできません。glitch-socがよりよい選択肢になるでしょう。 + +kmyblueは、Misskeyではありません。Misskeyは「楽しむ」をコンセプトにしていますが、kmyblueはMastodonの思想を受け継ぎ、炎上や喧騒を避けることのできる落ち着いた場所を目指しています。そのため、思想に合わない機能は実装しないか、大幅に弱体化しています。 + +kmyblueは、Fedibirdではありません。Fedibirdは大規模サーバー向けに設定していると思われる機能があり、例えば購読機能がその代表例です。Fedibirdの購読は擬似的なフォロー体験を与えるものですが、本物のフォローではないため、購読対象の投稿が配送されることを確約したものではありません。小規模サーバーだとかえって不便になる機能を、kmyblueは避けています。 + +## kmyblueの独自機能 + +以下に列挙したものはあくまで代表的なものです。これ以外にも、細かい仕様変更などが多数含まれます。 + +- 絵文字リアクション +- ローカル公開(Local Public)(リモートサーバーの連合タイムラインには流れませんが、フォロワーのホームタイムラインには流れます。**ローカル限定とは異なります**) +- ブックマークの分類 +- 自分の投稿を検索できる人を投稿ごとに設定(検索許可・Searchability) +- 投稿の引用、ひかえめな引用(参照) +- ドメイン・アカウント・キーワードなど特定条件を満たした投稿を記録する機能(購読・アンテナ) +- フォロワーの一部を指名して投稿を送る機能(サークル)(ダイレクトメッセージとは異なります) +- リスト新着投稿の通知 +- 投稿のフィルタリングにおいて、自分がフォローしている相手の投稿を除外 +- フォロー・フォロワー数を隠す機能 +- 指定した時間が経過したあとに投稿を自動削除する機能 +- モデレーション機能の拡張 + +## 英語のサポートについて + +kmyblueのメイン開発者である[雪あすか](https://kmy.blue/@askyq)は、英語の読み書きがほとんどできません。そのため、ドキュメントの英語化、海外向け公式アカウントの新設などを行う予定はありません。 + +要望やバグ報告はIssueに書いて構いませんが、Issue画面内の説明やテンプレートはすべて日本語になっています。投稿が難しければ、Discussionに投稿してください。こちらで必要と判断したものは、改めてIssueとして起票します。 + +そのほか開発者へ質問があれば、[@askyq@kmy.blue](https://kmy.blue/@askyq)へ英語のまま送ってください。 + +ただしkmyblueのドキュメント、[@askyq@kmy.blue](https://kmy.blue/@askyq)内のkmyblueフォークに関係する投稿を、許可なく翻訳して公開することは問題ありません。 + +## 開発者のアカウントについて + +kmyblueのメイン開発者である[雪あすか](https://kmy.blue/@askyq)は、用途別にアカウントを分けるようなことはせず、すべての発言を1つのアカウントで行っています。そのため、kmyblueの開発だけでなく、成人向け同人作品の話も混ざっています。 + +このうち、公開範囲「公開」「ローカル公開」「非収載」であるkmyblueフォークの開発に関係する投稿に限り抽出し、翻訳の有無に関係なく公開することを許可します。これはkmyblueフォークの利用者にとって公共性の高いコンテンツであると思われます。これは、日本と欧米では一般的に考えられている児童ポルノの基準が異なり、欧米のサーバーの中にはこのアカウントをフォローしづらいものもあるという懸念を考慮したものです。 diff --git a/Vagrantfile b/Vagrantfile index ce456060cd..89f5536edc 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -174,7 +174,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| if config.vm.networks.any? { |type, options| type == :private_network } config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'actimeo=1'] else - config.vm.synced_folder ".", "/vagrant", type: "rsync", create: true, rsync__args: ["--verbose", "--archive", "--delete", "-z"] + config.vm.synced_folder ".", "/vagrant" end # Otherwise, you can access the site at http://localhost:3000 and http://localhost:4000 , http://localhost:8080 diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index c80db3500d..ab1b98e646 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -49,7 +49,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def collection_presenter ActivityPub::CollectionPresenter.new( - id: ActivityPub::TagManager.instance.collection_uri_for(@account, params[:id]), + id: account_collection_url(@account, params[:id]), type: @type, size: @size, items: @items diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 171161d491..658cec9a4d 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -41,8 +41,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController end end - def outbox_url(...) - ActivityPub::TagManager.instance.outbox_uri_for(@account, ...) + def outbox_url(**) + if params[:account_username].present? + account_outbox_url(@account, **) + else + instance_actor_outbox_url(**) + end end def next_page diff --git a/app/controllers/admin/account_actions_controller.rb b/app/controllers/admin/account_actions_controller.rb index 91849811e3..e674bf55a0 100644 --- a/app/controllers/admin/account_actions_controller.rb +++ b/app/controllers/admin/account_actions_controller.rb @@ -34,8 +34,7 @@ module Admin end def resource_params - params - .expect(admin_account_action: [:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses]) + params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses) end end end diff --git a/app/controllers/admin/account_moderation_notes_controller.rb b/app/controllers/admin/account_moderation_notes_controller.rb index 7f65ced517..a3c4adf59a 100644 --- a/app/controllers/admin/account_moderation_notes_controller.rb +++ b/app/controllers/admin/account_moderation_notes_controller.rb @@ -29,8 +29,10 @@ module Admin private def resource_params - params - .expect(account_moderation_note: [:content, :target_account_id]) + params.require(:account_moderation_note).permit( + :content, + :target_account_id + ) end def set_account_moderation_note diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index a779a0cf51..a4d5018d0a 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -172,8 +172,7 @@ module Admin end def form_account_batch_params - params - .expect(form_account_batch: [:action, account_ids: []]) + params.require(:form_account_batch).permit(:action, account_ids: []) end def action_from_button diff --git a/app/controllers/admin/announcements/distributions_controller.rb b/app/controllers/admin/announcements/distributions_controller.rb deleted file mode 100644 index 4bd8769834..0000000000 --- a/app/controllers/admin/announcements/distributions_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Admin::Announcements::DistributionsController < Admin::BaseController - before_action :set_announcement - - def create - authorize @announcement, :distribute? - @announcement.touch(:notification_sent_at) - Admin::DistributeAnnouncementNotificationWorker.perform_async(@announcement.id) - redirect_to admin_announcements_path - end - - private - - def set_announcement - @announcement = Announcement.find(params[:announcement_id]) - end -end diff --git a/app/controllers/admin/announcements/previews_controller.rb b/app/controllers/admin/announcements/previews_controller.rb deleted file mode 100644 index d77f931a7f..0000000000 --- a/app/controllers/admin/announcements/previews_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class Admin::Announcements::PreviewsController < Admin::BaseController - before_action :set_announcement - - def show - authorize @announcement, :distribute? - @user_count = @announcement.scope_for_notification.count - end - - private - - def set_announcement - @announcement = Announcement.find(params[:announcement_id]) - end -end diff --git a/app/controllers/admin/announcements/tests_controller.rb b/app/controllers/admin/announcements/tests_controller.rb deleted file mode 100644 index f2457eb23a..0000000000 --- a/app/controllers/admin/announcements/tests_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class Admin::Announcements::TestsController < Admin::BaseController - before_action :set_announcement - - def create - authorize @announcement, :distribute? - UserMailer.announcement_published(current_user, @announcement).deliver_later! - redirect_to admin_announcements_path - end - - private - - def set_announcement - @announcement = Announcement.find(params[:announcement_id]) - end -end diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb index eaf84aab25..12230a6506 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -84,7 +84,6 @@ class Admin::AnnouncementsController < Admin::BaseController end def resource_params - params - .expect(announcement: [:text, :scheduled_at, :starts_at, :ends_at, :all_day]) + params.require(:announcement).permit(:text, :scheduled_at, :starts_at, :ends_at, :all_day) end end diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 14338dd293..48685db17a 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -7,14 +7,14 @@ module Admin layout 'admin' - before_action :set_referrer_policy_header + before_action :set_cache_headers after_action :verify_authorized private - def set_referrer_policy_header - response.headers['Referrer-Policy'] = 'same-origin' + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end def set_user diff --git a/app/controllers/admin/change_emails_controller.rb b/app/controllers/admin/change_emails_controller.rb index c923b94b1a..a689d3a530 100644 --- a/app/controllers/admin/change_emails_controller.rb +++ b/app/controllers/admin/change_emails_controller.rb @@ -41,8 +41,9 @@ module Admin end def resource_params - params - .expect(user: [:unconfirmed_email]) + params.require(:user).permit( + :unconfirmed_email + ) end end end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 596b167249..34368f08a2 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -67,13 +67,11 @@ module Admin end def resource_params - params - .expect(custom_emoji: [:shortcode, :image, :category_id, :visible_in_picker, :aliases_raw, :license]) + params.require(:custom_emoji).permit(:shortcode, :image, :category_id, :visible_in_picker, :aliases_raw, :license) end def update_params - params - .expect(custom_emoji: [:category_id, :visible_in_picker, :aliases_raw, :license]) + params.require(:custom_emoji).permit(:category_id, :visible_in_picker, :aliases_raw, :license) end def filtered_custom_emojis @@ -103,8 +101,7 @@ module Admin end def form_custom_emoji_batch_params - params - .expect(form_custom_emoji_batch: [:action, :category_id, :category_name, custom_emoji_ids: []]) + params.require(:form_custom_emoji_batch).permit(:action, :category_id, :category_name, custom_emoji_ids: []) end end end diff --git a/app/controllers/admin/domain_allows_controller.rb b/app/controllers/admin/domain_allows_controller.rb index 913c1a8246..b0f139e3a8 100644 --- a/app/controllers/admin/domain_allows_controller.rb +++ b/app/controllers/admin/domain_allows_controller.rb @@ -37,7 +37,6 @@ class Admin::DomainAllowsController < Admin::BaseController end def resource_params - params - .expect(domain_allow: [:domain]) + params.require(:domain_allow).permit(:domain) end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 520db814f2..78d2a2da28 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -35,9 +35,7 @@ module Admin rescue Mastodon::NotPermittedError flash[:alert] = I18n.t('admin.domain_blocks.not_permitted') else - flash[:notice] = I18n.t('admin.domain_blocks.created_msg') - ensure - redirect_to admin_instances_path(limited: '1') + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') end def new @@ -126,14 +124,9 @@ module Admin end def form_domain_block_batch_params - params - .expect( - form_domain_block_batch: [ - domain_blocks_attributes: [[:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate, - :reject_favourite, :reject_reply_exclude_followers, :reject_send_sensitive, :reject_hashtag, - :reject_straight_follow, :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, :hidden]], - ] - ) + params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply_exclude_followers, + :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :reject_friend, :block_trends, :detect_invalid_subscription, + :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden]) end def action_from_button diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 12f221164f..9501ebd63a 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -62,13 +62,11 @@ module Admin end def resource_params - params - .expect(email_domain_block: [:domain, :allow_with_approval, other_domains: []]) + params.require(:email_domain_block).permit(:domain, :allow_with_approval, other_domains: []) end def form_email_domain_block_batch_params - params - .expect(form_email_domain_block_batch: [email_domain_block_ids: []]) + params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: []) end def action_from_button diff --git a/app/controllers/admin/fasp/debug/callbacks_controller.rb b/app/controllers/admin/fasp/debug/callbacks_controller.rb deleted file mode 100644 index 28aba5e489..0000000000 --- a/app/controllers/admin/fasp/debug/callbacks_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class Admin::Fasp::Debug::CallbacksController < Admin::BaseController - def index - authorize [:admin, :fasp, :provider], :update? - - @callbacks = Fasp::DebugCallback - .includes(:fasp_provider) - .order(created_at: :desc) - end - - def destroy - authorize [:admin, :fasp, :provider], :update? - - callback = Fasp::DebugCallback.find(params[:id]) - callback.destroy - - redirect_to admin_fasp_debug_callbacks_path - end -end diff --git a/app/controllers/admin/fasp/debug_calls_controller.rb b/app/controllers/admin/fasp/debug_calls_controller.rb deleted file mode 100644 index 1e1b6dbf3c..0000000000 --- a/app/controllers/admin/fasp/debug_calls_controller.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -class Admin::Fasp::DebugCallsController < Admin::BaseController - before_action :set_provider - - def create - authorize [:admin, @provider], :update? - - @provider.perform_debug_call - - redirect_to admin_fasp_providers_path - end - - private - - def set_provider - @provider = Fasp::Provider.find(params[:provider_id]) - end -end diff --git a/app/controllers/admin/fasp/providers_controller.rb b/app/controllers/admin/fasp/providers_controller.rb deleted file mode 100644 index 4f1f1271bf..0000000000 --- a/app/controllers/admin/fasp/providers_controller.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -class Admin::Fasp::ProvidersController < Admin::BaseController - before_action :set_provider, only: [:show, :edit, :update, :destroy] - - def index - authorize [:admin, :fasp, :provider], :index? - - @providers = Fasp::Provider.order(confirmed: :asc, created_at: :desc) - end - - def show - authorize [:admin, @provider], :show? - end - - def edit - authorize [:admin, @provider], :update? - end - - def update - authorize [:admin, @provider], :update? - - if @provider.update(provider_params) - redirect_to admin_fasp_providers_path - else - render :edit - end - end - - def destroy - authorize [:admin, @provider], :destroy? - - @provider.destroy - - redirect_to admin_fasp_providers_path - end - - private - - def provider_params - params.expect(fasp_provider: [capabilities_attributes: {}]) - end - - def set_provider - @provider = Fasp::Provider.find(params[:id]) - end -end diff --git a/app/controllers/admin/fasp/registrations_controller.rb b/app/controllers/admin/fasp/registrations_controller.rb deleted file mode 100644 index 52c46c2eb6..0000000000 --- a/app/controllers/admin/fasp/registrations_controller.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -class Admin::Fasp::RegistrationsController < Admin::BaseController - before_action :set_provider - - def new - authorize [:admin, @provider], :create? - end - - def create - authorize [:admin, @provider], :create? - - @provider.update_info!(confirm: true) - - redirect_to edit_admin_fasp_provider_path(@provider) - end - - private - - def set_provider - @provider = Fasp::Provider.find(params[:provider_id]) - end -end diff --git a/app/controllers/admin/follow_recommendations_controller.rb b/app/controllers/admin/follow_recommendations_controller.rb index b060cfbe94..a54e41bd8c 100644 --- a/app/controllers/admin/follow_recommendations_controller.rb +++ b/app/controllers/admin/follow_recommendations_controller.rb @@ -37,8 +37,7 @@ module Admin end def form_account_batch_params - params - .expect(form_account_batch: [:action, account_ids: []]) + params.require(:form_account_batch).permit(:action, account_ids: []) end def filter_params diff --git a/app/controllers/admin/friend_servers_controller.rb b/app/controllers/admin/friend_servers_controller.rb index ec41ba672c..729d3b3912 100644 --- a/app/controllers/admin/friend_servers_controller.rb +++ b/app/controllers/admin/friend_servers_controller.rb @@ -79,11 +79,11 @@ module Admin end def resource_params - params.expect(friend_domain: [:domain, :inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts]) + params.require(:friend_domain).permit(:domain, :inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts) end def update_resource_params - params.expect(friend_domain: [:inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts]) + params.require(:friend_domain).permit(:inbox_url, :available, :pseudo_relay, :delivery_local, :unlocked, :allow_all_posts) end def warn_signatures_not_enabled! diff --git a/app/controllers/admin/invites_controller.rb b/app/controllers/admin/invites_controller.rb index ac4ee35271..614e2a32d0 100644 --- a/app/controllers/admin/invites_controller.rb +++ b/app/controllers/admin/invites_controller.rb @@ -39,8 +39,7 @@ module Admin private def resource_params - params - .expect(invite: [:max_uses, :expires_in]) + params.require(:invite).permit(:max_uses, :expires_in) end def filtered_invites diff --git a/app/controllers/admin/ip_blocks_controller.rb b/app/controllers/admin/ip_blocks_controller.rb index afabda1b88..1bd7ec8059 100644 --- a/app/controllers/admin/ip_blocks_controller.rb +++ b/app/controllers/admin/ip_blocks_controller.rb @@ -44,8 +44,7 @@ module Admin private def resource_params - params - .expect(ip_block: [:ip, :severity, :comment, :expires_in]) + params.require(:ip_block).permit(:ip, :severity, :comment, :expires_in) end def action_from_button @@ -53,8 +52,7 @@ module Admin end def form_ip_block_batch_params - params - .expect(form_ip_block_batch: [ip_block_ids: []]) + params.require(:form_ip_block_batch).permit(ip_block_ids: []) end end end diff --git a/app/controllers/admin/ng_rules_controller.rb b/app/controllers/admin/ng_rules_controller.rb index 0bdda41c0c..f37424cced 100644 --- a/app/controllers/admin/ng_rules_controller.rb +++ b/app/controllers/admin/ng_rules_controller.rb @@ -82,16 +82,16 @@ module Admin end def resource_params - params.expect(ng_rule: [:title, :expires_in, :available, :account_domain, :account_username, :account_display_name, - :account_note, :account_field_name, :account_field_value, :account_avatar_state, - :account_header_state, :account_include_local, :status_spoiler_text, :status_text, :status_tag, - :status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state, - :status_mention_state, :status_reference_state, - :status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold, - :status_mention_threshold, :status_allow_follower_mention, - :reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain, - :status_reference_threshold, :account_allow_followed_by_local, :record_history_also_local, - status_visibility: [], status_searchability: [], reaction_type: []]) + params.require(:ng_rule).permit(:title, :expires_in, :available, :account_domain, :account_username, :account_display_name, + :account_note, :account_field_name, :account_field_value, :account_avatar_state, + :account_header_state, :account_include_local, :status_spoiler_text, :status_text, :status_tag, + :status_sensitive_state, :status_cw_state, :status_media_state, :status_poll_state, + :status_mention_state, :status_reference_state, + :status_quote_state, :status_reply_state, :status_media_threshold, :status_poll_threshold, + :status_mention_threshold, :status_allow_follower_mention, + :reaction_allow_follower, :emoji_reaction_name, :emoji_reaction_origin_domain, + :status_reference_threshold, :account_allow_followed_by_local, :record_history_also_local, + status_visibility: [], status_searchability: [], reaction_type: []) end def test_words! diff --git a/app/controllers/admin/ng_words/keywords_controller.rb b/app/controllers/admin/ng_words/keywords_controller.rb index 10969204e8..9af38fab7b 100644 --- a/app/controllers/admin/ng_words/keywords_controller.rb +++ b/app/controllers/admin/ng_words/keywords_controller.rb @@ -21,10 +21,6 @@ module Admin false end - def avoid_save? - true - end - private def after_update_redirect_path diff --git a/app/controllers/admin/ng_words_controller.rb b/app/controllers/admin/ng_words_controller.rb index 9e437f8c8b..a70a435fa4 100644 --- a/app/controllers/admin/ng_words_controller.rb +++ b/app/controllers/admin/ng_words_controller.rb @@ -13,12 +13,6 @@ module Admin return unless validate - if avoid_save? - flash[:notice] = I18n.t('generic.changes_saved_msg') - redirect_to after_update_redirect_path - return - end - @admin_settings = Form::AdminSettings.new(settings_params) if @admin_settings.save @@ -39,18 +33,14 @@ module Admin admin_ng_words_path end - def avoid_save? - false - end - private def settings_params - params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end def settings_params_test - params.expect(form_admin_settings: [ng_words_test: [keywords: [], regexps: [], strangers: [], temporary_ids: []]])['ng_words_test'] + params.require(:form_admin_settings)[:ng_words_test] end end end diff --git a/app/controllers/admin/relays_controller.rb b/app/controllers/admin/relays_controller.rb index 9a796949de..f05255adb6 100644 --- a/app/controllers/admin/relays_controller.rb +++ b/app/controllers/admin/relays_controller.rb @@ -57,8 +57,7 @@ module Admin end def resource_params - params - .expect(relay: [:inbox_url]) + params.require(:relay).permit(:inbox_url) end def warn_signatures_not_enabled! diff --git a/app/controllers/admin/report_notes_controller.rb b/app/controllers/admin/report_notes_controller.rb index 10dbe846e4..6b16c29fc7 100644 --- a/app/controllers/admin/report_notes_controller.rb +++ b/app/controllers/admin/report_notes_controller.rb @@ -47,8 +47,10 @@ module Admin end def resource_params - params - .expect(report_note: [:content, :report_id]) + params.require(:report_note).permit( + :content, + :report_id + ) end def set_report_note diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index 2f9af8a6fc..bcfc11159c 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -61,8 +61,7 @@ module Admin end def resource_params - params - .expect(user_role: [:name, :color, :highlighted, :position, permissions_as_keys: []]) + params.require(:user_role).permit(:name, :color, :highlighted, :position, permissions_as_keys: []) end end end diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb index 289b6a98c3..b8def22ba3 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -53,8 +53,7 @@ module Admin end def resource_params - params - .expect(rule: [:text, :hint, :priority]) + params.require(:rule).permit(:text, :hint, :priority) end end end diff --git a/app/controllers/admin/sensitive_words_controller.rb b/app/controllers/admin/sensitive_words_controller.rb index 716dcc708a..24cdd4efcb 100644 --- a/app/controllers/admin/sensitive_words_controller.rb +++ b/app/controllers/admin/sensitive_words_controller.rb @@ -37,7 +37,7 @@ module Admin end def settings_params - params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end def settings_params_test diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 2ae5ec8255..338a3638c4 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -28,8 +28,7 @@ module Admin end def settings_params - params - .expect(form_admin_settings: [*Form::AdminSettings::KEYS]) + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end end end diff --git a/app/controllers/admin/software_updates_controller.rb b/app/controllers/admin/software_updates_controller.rb index c9be97eb71..52d8cb41e6 100644 --- a/app/controllers/admin/software_updates_controller.rb +++ b/app/controllers/admin/software_updates_controller.rb @@ -6,7 +6,7 @@ module Admin def index authorize :software_update, :index? - @software_updates = SoftwareUpdate.by_version.filter(&:pending?) + @software_updates = SoftwareUpdate.all.sort_by(&:gem_version) end private diff --git a/app/controllers/admin/special_domains_controller.rb b/app/controllers/admin/special_domains_controller.rb index b36fe28d6e..0ddbf26786 100644 --- a/app/controllers/admin/special_domains_controller.rb +++ b/app/controllers/admin/special_domains_controller.rb @@ -28,7 +28,7 @@ module Admin end def settings_params - params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end end end diff --git a/app/controllers/admin/special_instances_controller.rb b/app/controllers/admin/special_instances_controller.rb index a16bae13ef..3fd35d474e 100644 --- a/app/controllers/admin/special_instances_controller.rb +++ b/app/controllers/admin/special_instances_controller.rb @@ -28,7 +28,7 @@ module Admin end def settings_params - params.expect(form_admin_settings: [*Form::AdminSettings::KEYS]) + params.require(:form_admin_settings).permit(*Form::AdminSettings::KEYS) end end end diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb index 956950fe0d..047f79e04b 100644 --- a/app/controllers/admin/statuses_controller.rb +++ b/app/controllers/admin/statuses_controller.rb @@ -98,8 +98,7 @@ module Admin helper_method :batched_ordered_status_edits def admin_status_batch_action_params - params - .expect(admin_status_batch_action: [status_ids: []]) + params.require(:admin_status_batch_action).permit(status_ids: []) end def after_create_redirect_path diff --git a/app/controllers/admin/tags_controller.rb b/app/controllers/admin/tags_controller.rb index a7bfd64794..4759d15bc4 100644 --- a/app/controllers/admin/tags_controller.rb +++ b/app/controllers/admin/tags_controller.rb @@ -37,8 +37,7 @@ module Admin end def tag_params - params - .expect(tag: [:name, :display_name, :trendable, :usable, :listable]) + params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable) end def filtered_tags diff --git a/app/controllers/admin/terms_of_service/distributions_controller.rb b/app/controllers/admin/terms_of_service/distributions_controller.rb deleted file mode 100644 index c639b083dd..0000000000 --- a/app/controllers/admin/terms_of_service/distributions_controller.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::DistributionsController < Admin::BaseController - before_action :set_terms_of_service - - def create - authorize @terms_of_service, :distribute? - @terms_of_service.touch(:notification_sent_at) - Admin::DistributeTermsOfServiceNotificationWorker.perform_async(@terms_of_service.id) - redirect_to admin_terms_of_service_index_path - end - - private - - def set_terms_of_service - @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) - end -end diff --git a/app/controllers/admin/terms_of_service/drafts_controller.rb b/app/controllers/admin/terms_of_service/drafts_controller.rb deleted file mode 100644 index 0c67eb9df8..0000000000 --- a/app/controllers/admin/terms_of_service/drafts_controller.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::DraftsController < Admin::BaseController - before_action :set_terms_of_service - - def show - authorize :terms_of_service, :create? - end - - def update - authorize @terms_of_service, :update? - - @terms_of_service.published_at = Time.now.utc if params[:action_type] == 'publish' - - if @terms_of_service.update(resource_params) - log_action(:publish, @terms_of_service) if @terms_of_service.published? - redirect_to @terms_of_service.published? ? admin_terms_of_service_index_path : admin_terms_of_service_draft_path - else - render :show - end - end - - private - - def set_terms_of_service - @terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now) - end - - def current_terms_of_service - TermsOfService.live.first - end - - def resource_params - params - .expect(terms_of_service: [:text, :changelog, :effective_date]) - end -end diff --git a/app/controllers/admin/terms_of_service/generates_controller.rb b/app/controllers/admin/terms_of_service/generates_controller.rb deleted file mode 100644 index 0edc87893e..0000000000 --- a/app/controllers/admin/terms_of_service/generates_controller.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::GeneratesController < Admin::BaseController - before_action :set_instance_presenter - - def show - authorize :terms_of_service, :create? - - @generator = TermsOfService::Generator.new( - domain: @instance_presenter.domain, - admin_email: @instance_presenter.contact.email - ) - end - - def create - authorize :terms_of_service, :create? - - @generator = TermsOfService::Generator.new(resource_params) - - if @generator.valid? - TermsOfService.create!(text: @generator.render) - redirect_to admin_terms_of_service_draft_path - else - render :show - end - end - - private - - def set_instance_presenter - @instance_presenter = InstancePresenter.new - end - - def resource_params - params - .expect(terms_of_service_generator: [*TermsOfService::Generator::VARIABLES]) - end -end diff --git a/app/controllers/admin/terms_of_service/histories_controller.rb b/app/controllers/admin/terms_of_service/histories_controller.rb deleted file mode 100644 index 8f12341aea..0000000000 --- a/app/controllers/admin/terms_of_service/histories_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::HistoriesController < Admin::BaseController - def show - authorize :terms_of_service, :index? - @terms_of_service = TermsOfService.published.all - end -end diff --git a/app/controllers/admin/terms_of_service/previews_controller.rb b/app/controllers/admin/terms_of_service/previews_controller.rb deleted file mode 100644 index 0a1a966751..0000000000 --- a/app/controllers/admin/terms_of_service/previews_controller.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::PreviewsController < Admin::BaseController - before_action :set_terms_of_service - - def show - authorize @terms_of_service, :distribute? - @user_count = @terms_of_service.scope_for_notification.count - end - - private - - def set_terms_of_service - @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) - end -end diff --git a/app/controllers/admin/terms_of_service/tests_controller.rb b/app/controllers/admin/terms_of_service/tests_controller.rb deleted file mode 100644 index e2483c1005..0000000000 --- a/app/controllers/admin/terms_of_service/tests_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfService::TestsController < Admin::BaseController - before_action :set_terms_of_service - - def create - authorize @terms_of_service, :distribute? - UserMailer.terms_of_service_changed(current_user, @terms_of_service).deliver_later! - redirect_to admin_terms_of_service_preview_path(@terms_of_service) - end - - private - - def set_terms_of_service - @terms_of_service = TermsOfService.find(params[:terms_of_service_id]) - end -end diff --git a/app/controllers/admin/terms_of_service_controller.rb b/app/controllers/admin/terms_of_service_controller.rb deleted file mode 100644 index 10aa5c66ca..0000000000 --- a/app/controllers/admin/terms_of_service_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class Admin::TermsOfServiceController < Admin::BaseController - def index - authorize :terms_of_service, :index? - @terms_of_service = TermsOfService.published.first - end -end diff --git a/app/controllers/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/admin/trends/links/preview_card_providers_controller.rb index 5a650d5d8c..5e4b4084f8 100644 --- a/app/controllers/admin/trends/links/preview_card_providers_controller.rb +++ b/app/controllers/admin/trends/links/preview_card_providers_controller.rb @@ -31,8 +31,7 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll end def trends_preview_card_provider_batch_params - params - .expect(trends_preview_card_provider_batch: [:action, preview_card_provider_ids: []]) + params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: []) end def action_from_button diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb index 68aa73c992..65eca11c7f 100644 --- a/app/controllers/admin/trends/links_controller.rb +++ b/app/controllers/admin/trends/links_controller.rb @@ -31,8 +31,7 @@ class Admin::Trends::LinksController < Admin::BaseController end def trends_preview_card_batch_params - params - .expect(trends_preview_card_batch: [:action, preview_card_ids: []]) + params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: []) end def action_from_button diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb index 873d777fe3..682fe70bb5 100644 --- a/app/controllers/admin/trends/statuses_controller.rb +++ b/app/controllers/admin/trends/statuses_controller.rb @@ -31,8 +31,7 @@ class Admin::Trends::StatusesController < Admin::BaseController end def trends_status_batch_params - params - .expect(trends_status_batch: [:action, status_ids: []]) + params.require(:trends_status_batch).permit(:action, status_ids: []) end def action_from_button diff --git a/app/controllers/admin/trends/tags_controller.rb b/app/controllers/admin/trends/tags_controller.rb index 1ccd740686..fcd23fbf66 100644 --- a/app/controllers/admin/trends/tags_controller.rb +++ b/app/controllers/admin/trends/tags_controller.rb @@ -31,8 +31,7 @@ class Admin::Trends::TagsController < Admin::BaseController end def trends_tag_batch_params - params - .expect(trends_tag_batch: [:action, tag_ids: []]) + params.require(:trends_tag_batch).permit(:action, tag_ids: []) end def action_from_button diff --git a/app/controllers/admin/users/roles_controller.rb b/app/controllers/admin/users/roles_controller.rb index e8b58de504..f5dfc643d4 100644 --- a/app/controllers/admin/users/roles_controller.rb +++ b/app/controllers/admin/users/roles_controller.rb @@ -28,8 +28,7 @@ module Admin end def resource_params - params - .expect(user: [:role_id]) + params.require(:user).permit(:role_id) end end end diff --git a/app/controllers/admin/warning_presets_controller.rb b/app/controllers/admin/warning_presets_controller.rb index dcf88294ee..efbf65b119 100644 --- a/app/controllers/admin/warning_presets_controller.rb +++ b/app/controllers/admin/warning_presets_controller.rb @@ -52,8 +52,7 @@ module Admin end def warning_preset_params - params - .expect(account_warning_preset: [:title, :text]) + params.require(:account_warning_preset).permit(:title, :text) end end end diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index 31db369637..f1aad7c4b5 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -74,8 +74,7 @@ module Admin end def resource_params - params - .expect(webhook: [:url, :template, events: []]) + params.require(:webhook).permit(:url, :template, events: []) end end end diff --git a/app/controllers/api/fasp/base_controller.rb b/app/controllers/api/fasp/base_controller.rb deleted file mode 100644 index 690f7e419a..0000000000 --- a/app/controllers/api/fasp/base_controller.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -class Api::Fasp::BaseController < ApplicationController - class Error < ::StandardError; end - - DIGEST_PATTERN = /sha-256=:(.*?):/ - KEYID_PATTERN = /keyid="(.*?)"/ - - attr_reader :current_provider - - skip_forgery_protection - - before_action :check_fasp_enabled - before_action :require_authentication - after_action :sign_response - - private - - def require_authentication - validate_content_digest! - validate_signature! - rescue Error, Linzer::Error, ActiveRecord::RecordNotFound => e - logger.debug("FASP Authentication error: #{e}") - authentication_error - end - - def authentication_error - respond_to do |format| - format.json { head 401 } - end - end - - def validate_content_digest! - content_digest_header = request.headers['content-digest'] - raise Error, 'content-digest missing' if content_digest_header.blank? - - digest_received = content_digest_header.match(DIGEST_PATTERN)[1] - - digest_computed = OpenSSL::Digest.base64digest('sha256', request.body&.string || '') - - raise Error, 'content-digest does not match' if digest_received != digest_computed - end - - def validate_signature! - signature_input = request.headers['signature-input']&.encode('UTF-8') - raise Error, 'signature-input is missing' if signature_input.blank? - - keyid = signature_input.match(KEYID_PATTERN)[1] - provider = Fasp::Provider.find(keyid) - linzer_request = Linzer.new_request( - request.method, - request.original_url, - {}, - { - 'content-digest' => request.headers['content-digest'], - 'signature-input' => signature_input, - 'signature' => request.headers['signature'], - } - ) - message = Linzer::Message.new(linzer_request) - key = Linzer.new_ed25519_public_key(provider.provider_public_key_pem, keyid) - signature = Linzer::Signature.build(message.headers) - Linzer.verify(key, message, signature) - @current_provider = provider - end - - def sign_response - response.headers['content-digest'] = "sha-256=:#{OpenSSL::Digest.base64digest('sha256', response.body || '')}:" - - linzer_response = Linzer.new_response(response.body, response.status, { 'content-digest' => response.headers['content-digest'] }) - message = Linzer::Message.new(linzer_response) - key = Linzer.new_ed25519_key(current_provider.server_private_key_pem) - signature = Linzer.sign(key, message, %w(@status content-digest)) - - response.headers.merge!(signature.to_h) - end - - def check_fasp_enabled - raise ActionController::RoutingError unless Mastodon::Feature.fasp_enabled? - end -end diff --git a/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb b/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb deleted file mode 100644 index 794e53f095..0000000000 --- a/app/controllers/api/fasp/debug/v0/callback/responses_controller.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -class Api::Fasp::Debug::V0::Callback::ResponsesController < Api::Fasp::BaseController - def create - Fasp::DebugCallback.create( - fasp_provider: current_provider, - ip: request.remote_ip, - request_body: request.raw_post - ) - - respond_to do |format| - format.json { head 201 } - end - end -end diff --git a/app/controllers/api/fasp/registrations_controller.rb b/app/controllers/api/fasp/registrations_controller.rb deleted file mode 100644 index fecc992fec..0000000000 --- a/app/controllers/api/fasp/registrations_controller.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -class Api::Fasp::RegistrationsController < Api::Fasp::BaseController - skip_before_action :require_authentication - - def create - @current_provider = Fasp::Provider.create!( - name: params[:name], - base_url: params[:baseUrl], - remote_identifier: params[:serverId], - provider_public_key_base64: params[:publicKey] - ) - - render json: registration_confirmation - end - - private - - def registration_confirmation - { - faspId: current_provider.id.to_s, - publicKey: current_provider.server_public_key_base64, - registrationCompletionUri: new_admin_fasp_provider_registration_url(current_provider), - } - end -end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index bdd7732b87..7488fdec7c 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController @account = current_account UpdateAccountService.new.call(@account, account_params, raise_error: true) current_user.update(user_params) if user_params - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer rescue ActiveRecord::RecordInvalid => e render json: ValidationErrorFormatter.new(e).as_json, status: 422 @@ -34,7 +34,6 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController :searchability, :hide_collections, :indexable, - attribution_domains: [], fields_attributes: [:name, :value] ) end diff --git a/app/controllers/api/v1/accounts/endorsements_controller.rb b/app/controllers/api/v1/accounts/endorsements_controller.rb deleted file mode 100644 index 1e21994a90..0000000000 --- a/app/controllers/api/v1/accounts/endorsements_controller.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::EndorsementsController < Api::BaseController - include Authorization - - before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :index - before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index - before_action :require_user!, except: :index - before_action :set_account - before_action :set_endorsed_accounts, only: :index - after_action :insert_pagination_headers, only: :index - - def index - cache_if_unauthenticated! - render json: @endorsed_accounts, each_serializer: REST::AccountSerializer - end - - def create - AccountPin.find_or_create_by!(account: current_account, target_account: @account) - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter - end - - def destroy - pin = AccountPin.find_by(account: current_account, target_account: @account) - pin&.destroy! - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter - end - - private - - def set_account - @account = Account.find(params[:account_id]) - end - - def set_endorsed_accounts - @endorsed_accounts = @account.unavailable? ? [] : paginated_endorsed_accounts - end - - def paginated_endorsed_accounts - @account.endorsed_accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id( - limit_param(DEFAULT_ACCOUNTS_LIMIT), - params[:max_id], - params[:since_id] - ) - end - - def relationships_presenter - AccountRelationshipsPresenter.new([@account], current_user.account_id) - end - - def next_path - api_v1_account_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue? - end - - def prev_path - api_v1_account_endorsements_url pagination_params(since_id: pagination_since_id) unless @endorsed_accounts.empty? - end - - def pagination_collection - @endorsed_accounts - end - - def records_continue? - @endorsed_accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) - end -end diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb index f95846366c..0101fb469b 100644 --- a/app/controllers/api/v1/accounts/featured_tags_controller.rb +++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb @@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController end def set_featured_tags - @featured_tags = @account.unavailable? ? [] : @account.featured_tags + @featured_tags = @account.suspended? ? [] : @account.featured_tags end end diff --git a/app/controllers/api/v1/accounts/identity_proofs_controller.rb b/app/controllers/api/v1/accounts/identity_proofs_controller.rb index 02a45e8758..48f293f47a 100644 --- a/app/controllers/api/v1/accounts/identity_proofs_controller.rb +++ b/app/controllers/api/v1/accounts/identity_proofs_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class Api::V1::Accounts::IdentityProofsController < Api::BaseController - include DeprecationConcern - - deprecate_api '2022-03-30' - before_action :require_user! before_action :set_account diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb new file mode 100644 index 0000000000..0eb13c048c --- /dev/null +++ b/app/controllers/api/v1/accounts/pins_controller.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::PinsController < Api::BaseController + include Authorization + + before_action -> { doorkeeper_authorize! :write, :'write:accounts' } + before_action :require_user! + before_action :set_account + + def create + AccountPin.find_or_create_by!(account: current_account, target_account: @account) + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + def destroy + pin = AccountPin.find_by(account: current_account, target_account: @account) + pin&.destroy! + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def relationships_presenter + AccountRelationshipsPresenter.new([@account], current_user.account_id) + end +end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 46838aeb66..6bef6a3768 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -124,7 +124,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth) + params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code) end def invite diff --git a/app/controllers/api/v1/antennas_controller.rb b/app/controllers/api/v1/antennas_controller.rb index 4040263c00..37bfb7f552 100644 --- a/app/controllers/api/v1/antennas_controller.rb +++ b/app/controllers/api/v1/antennas_controller.rb @@ -21,7 +21,7 @@ class Api::V1::AntennasController < Api::BaseController end def create - @antenna = Antenna.create!(antenna_params.merge(account: current_account)) + @antenna = Antenna.create!(antenna_params.merge(account: current_account, list_id: 0)) render json: @antenna, serializer: REST::AntennaSerializer end @@ -42,6 +42,6 @@ class Api::V1::AntennasController < Api::BaseController end def antenna_params - params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog, :favourite) + params.permit(:title, :list_id, :insert_feeds, :stl, :ltl, :with_media_only, :ignore_reblog) end end diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index f8d91c5f7f..c97e9720ad 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true class Api::V1::FiltersController < Api::BaseController - include DeprecationConcern - - deprecate_api '2022-11-14' - before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show] before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show] before_action :require_user! diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index bf96fbaaa8..7ec94312f4 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -31,7 +31,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr end def show_domain_blocks_to_user? - Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved? + Setting.show_domain_blocks == 'users' && user_signed_in? end def set_domain_blocks @@ -47,6 +47,6 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr end def show_rationale_for_user? - Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved? + Setting.show_domain_blocks_rationale == 'users' && user_signed_in? end end diff --git a/app/controllers/api/v1/instances/terms_of_services_controller.rb b/app/controllers/api/v1/instances/terms_of_services_controller.rb deleted file mode 100644 index 0a861dd7bb..0000000000 --- a/app/controllers/api/v1/instances/terms_of_services_controller.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseController - before_action :set_terms_of_service - - def show - cache_even_if_authenticated! - render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer - end - - private - - def set_terms_of_service - @terms_of_service = begin - if params[:date].present? - TermsOfService.published.find_by!(effective_date: params[:date]) - else - TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet - end - end - end -end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index e01267c000..49da75ed28 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,9 +1,15 @@ # frozen_string_literal: true -class Api::V1::InstancesController < Api::V2::InstancesController - include DeprecationConcern +class Api::V1::InstancesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + skip_around_action :set_locale - deprecate_api '2022-11-14' + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in limited federation mode + def current_user + super if limited_federation_mode? + end def show cache_even_if_authenticated! diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index b019ab6018..0bacd7fdb0 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -7,6 +7,10 @@ class Api::V1::ListsController < Api::BaseController before_action :require_user! before_action :set_list, except: [:index, :create] + rescue_from ArgumentError do |e| + render json: { error: e.to_s }, status: 422 + end + def index @lists = List.where(account: current_account).all render json: @lists, each_serializer: REST::ListSerializer @@ -34,16 +38,6 @@ class Api::V1::ListsController < Api::BaseController render_empty end - def favourite - @list.favourite! - render json: @list, serializer: REST::ListSerializer - end - - def unfavourite - @list.unfavourite! - render json: @list, serializer: REST::ListSerializer - end - private def set_list @@ -51,6 +45,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy, :exclusive, :notify, :favourite) + params.permit(:title, :replies_policy, :exclusive, :notify) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index c427e055ea..5ea26d55bd 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -3,8 +3,8 @@ class Api::V1::MediaController < Api::BaseController before_action -> { doorkeeper_authorize! :write, :'write:media' } before_action :require_user! - before_action :set_media_attachment, except: [:create, :destroy] - before_action :check_processing, except: [:create, :destroy] + 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 @@ -25,15 +25,6 @@ class Api::V1::MediaController < Api::BaseController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment end - def destroy - @media_attachment = current_account.media_attachments.find(params[:id]) - - return render json: in_usage_error, status: 422 unless @media_attachment.status_id.nil? - - @media_attachment.destroy - render_empty - end - private def status_code_for_media_attachment @@ -63,8 +54,4 @@ class Api::V1::MediaController < Api::BaseController def processing_error { error: 'Error processing thumbnail for uploaded media' } end - - def in_usage_error - { error: 'Media attachment is currently used by a status' } - end end diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb index 2833687a38..ad1b82cb52 100644 --- a/app/controllers/api/v1/polls/votes_controller.rb +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -15,7 +15,7 @@ class Api::V1::Polls::VotesController < Api::BaseController private def set_poll - @poll = Poll.find(params[:poll_id]) + @poll = Poll.attached.find(params[:poll_id]) authorize @poll.status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index b4c25476e8..ffc70a8496 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -15,7 +15,7 @@ class Api::V1::PollsController < Api::BaseController private def set_poll - @poll = Poll.find(params[:id]) + @poll = Poll.attached.find(params[:id]) authorize @poll.status, :show? rescue Mastodon::NotPermittedError not_found diff --git a/app/controllers/api/v1/profile/avatars_controller.rb b/app/controllers/api/v1/profile/avatars_controller.rb index e6c954ed63..bc4d01a597 100644 --- a/app/controllers/api/v1/profile/avatars_controller.rb +++ b/app/controllers/api/v1/profile/avatars_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController def destroy @account = current_account UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end end diff --git a/app/controllers/api/v1/profile/headers_controller.rb b/app/controllers/api/v1/profile/headers_controller.rb index 4472a01b05..9f4daa2f77 100644 --- a/app/controllers/api/v1/profile/headers_controller.rb +++ b/app/controllers/api/v1/profile/headers_controller.rb @@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController def destroy @account = current_account UpdateAccountService.new.call(@account, { header: nil }, raise_error: true) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end end diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index f2c52f2846..e1ad89ee3e 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -21,7 +21,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController endpoint: subscription_params[:endpoint], key_p256dh: subscription_params[:keys][:p256dh], key_auth: subscription_params[:keys][:auth], - standard: subscription_params[:standard] || false, data: data_params, user_id: current_user.id, access_token_id: doorkeeper_token.id @@ -56,12 +55,12 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def subscription_params - params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]]) + params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) end def data_params return {} if params[:data].blank? - params.expect(data: [:policy, alerts: Notification::TYPES]) + params.require(:data).permit(:policy, alerts: Notification::TYPES) end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 1217b70752..534347d019 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -67,8 +67,6 @@ class Api::V1::StatusesController < Api::BaseController statuses = [@status] + @context.ancestors + @context.descendants + @context.references render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id) - - ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies? end def create @@ -127,7 +125,7 @@ class Api::V1::StatusesController < Api::BaseController @status.account.statuses_count = @status.account.statuses_count - 1 json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true - RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) }) + RemovalWorker.perform_async(@status.id, { 'redraft' => true }) render json: json end diff --git a/app/controllers/api/v1/suggestions_controller.rb b/app/controllers/api/v1/suggestions_controller.rb index df9346832f..9ba1cef63c 100644 --- a/app/controllers/api/v1/suggestions_controller.rb +++ b/app/controllers/api/v1/suggestions_controller.rb @@ -2,9 +2,6 @@ class Api::V1::SuggestionsController < Api::BaseController include Authorization - include DeprecationConcern - - deprecate_api '2021-05-16', only: [:index] before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index ecac3579fc..b15dd50131 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true class Api::V1::Trends::TagsController < Api::BaseController - include DeprecationConcern - before_action :set_tags after_action :insert_pagination_headers - DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i - - deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' } + DEFAULT_TAGS_LIMIT = 10 def index cache_if_unauthenticated! @@ -31,9 +27,7 @@ class Api::V1::Trends::TagsController < Api::BaseController end def tags_from_trends - scope = Trends.tags.query.allowed.in_locale(content_locale) - scope = scope.filtered_for(current_account) if user_signed_in? - scope + Trends.tags.query.allowed end def next_path diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index 62adf95260..8346e28830 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -1,16 +1,6 @@ # frozen_string_literal: true -class Api::V2::InstancesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? - skip_around_action :set_locale - - vary_by '' - - # Override `current_user` to avoid reading session cookies unless in limited federation mode - def current_user - super if limited_federation_mode? - end - +class Api::V2::InstancesController < Api::V1::InstancesController def show cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' diff --git a/app/controllers/api/v2/notifications_controller.rb b/app/controllers/api/v2/notifications_controller.rb index 848c361cfc..c070c0e5e7 100644 --- a/app/controllers/api/v2/notifications_controller.rb +++ b/app/controllers/api/v2/notifications_controller.rb @@ -46,7 +46,7 @@ class Api::V2::NotificationsController < Api::BaseController end def show - @notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take! + @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key]) presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification])) render json: presenter, serializer: REST::DedupNotificationGroupSerializer end @@ -57,7 +57,7 @@ class Api::V2::NotificationsController < Api::BaseController end def dismiss - current_account.notifications.by_group_key(params[:group_key]).destroy_all + current_account.notifications.where(group_key: params[:group_key]).destroy_all render_empty end @@ -80,31 +80,10 @@ class Api::V2::NotificationsController < Api::BaseController return [] if @notifications.empty? MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do - pagination_range = (@notifications.last.id)..@notifications.first.id - - # If the page is incomplete, we know we are on the last page - if incomplete_page? - if paginating_up? - pagination_range = @notifications.last.id...(params[:max_id]&.to_i) - else - range_start = params[:since_id]&.to_i - range_start += 1 unless range_start.nil? - pagination_range = range_start..(@notifications.first.id) - end - end - - NotificationGroup.from_notifications(@notifications, pagination_range: pagination_range, grouped_types: params[:grouped_types]) + NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types]) end end - def incomplete_page? - @notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT) - end - - def paginating_up? - params[:min_id].present? - end - def browserable_account_notifications current_account.notifications.without_suspended.browserable( types: Array(browserable_params[:types]), diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index 2711071b4a..f515961427 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -66,7 +66,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def subscription_params - @subscription_params ||= params.expect(subscription: [:standard, :endpoint, keys: [:auth, :p256dh]]) + @subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) end def web_push_subscription_params @@ -76,12 +76,11 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController endpoint: subscription_params[:endpoint], key_auth: subscription_params[:keys][:auth], key_p256dh: subscription_params[:keys][:p256dh], - standard: subscription_params[:standard] || false, user_id: active_session.user_id, } end def data_params - @data_params ||= params.expect(data: [:policy, alerts: Notification::TYPES]) + @data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1b071e8655..7a858ed059 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -70,13 +70,7 @@ class ApplicationController < ActionController::Base end def require_functional! - return if current_user.functional? - - if current_user.confirmed? - redirect_to edit_user_registration_path - else - redirect_to auth_setup_path - end + redirect_to edit_user_registration_path unless current_user.functional? end def skip_csrf_meta_tags? diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 0b6f5b3af4..4d94c80158 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -12,6 +12,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :set_sessions, only: [:edit, :update] before_action :set_strikes, only: [:edit, :update] before_action :require_not_suspended!, only: [:update] + before_action :set_cache_headers, only: [:edit, :update] before_action :set_rules, only: :new before_action :require_rules_acceptance!, only: :new before_action :set_registration_form_time, only: :new @@ -62,7 +63,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up) do |user_params| - user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth) + user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password) end end @@ -138,11 +139,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController set_locale { render :rules } end - def is_flashing_format? # rubocop:disable Naming/PredicateName - if params[:action] == 'create' - false # Disable flash messages for sign-up - else - super - end + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 5f9f133659..18603a32f2 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -73,7 +73,7 @@ class Auth::SessionsController < Devise::SessionsController end def user_params - params.expect(user: [:email, :password, :otp_attempt, :disable_css, credential: {}]) + params.require(:user).permit(:email, :password, :otp_attempt, :disable_css, credential: {}) end def login_page_params @@ -174,7 +174,7 @@ class Auth::SessionsController < Devise::SessionsController end def disable_custom_css? - user_params[:disable_css].present? && user_params[:disable_css] == '1' + user_params[:disable_css].present? && user_params[:disable_css] != '0' end def disable_custom_css!(user) diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 5e7b14646a..ad872dc607 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -18,7 +18,7 @@ class Auth::SetupController < ApplicationController if @user.update(user_params) @user.resend_confirmation_instructions unless @user.confirmed? - redirect_to auth_setup_path, notice: t('auth.setup.new_confirmation_instructions_sent') + redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent') else render :show end @@ -35,6 +35,6 @@ class Auth::SetupController < ApplicationController end def user_params - params.expect(user: [:email]) + params.require(:user).permit(:email) end end diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 076d19874b..5df1af5f2f 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -9,15 +9,13 @@ class BackupsController < ApplicationController before_action :authenticate_user! before_action :set_backup - BACKUP_LINK_TIMEOUT = 1.hour.freeze - def download case Paperclip::Attachment.default_options[:storage] when :s3, :azure - redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true + redirect_to @backup.dump.expiring_url(10), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true + redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true else redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end diff --git a/app/controllers/concerns/admin/export_controller_concern.rb b/app/controllers/concerns/admin/export_controller_concern.rb index ce03b2a24a..6228ae67fe 100644 --- a/app/controllers/concerns/admin/export_controller_concern.rb +++ b/app/controllers/concerns/admin/export_controller_concern.rb @@ -24,6 +24,6 @@ module Admin::ExportControllerConcern end def import_params - params.expect(admin_import: [:data]) + params.require(:admin_import).permit(:data) end end diff --git a/app/controllers/concerns/challengable_concern.rb b/app/controllers/concerns/challengable_concern.rb index 7fbc469bdf..c8d1a0bef7 100644 --- a/app/controllers/concerns/challengable_concern.rb +++ b/app/controllers/concerns/challengable_concern.rb @@ -58,6 +58,6 @@ module ChallengableConcern end def challenge_params - params.expect(form_challenge: [:current_password, :return_to]) + params.require(:form_challenge).permit(:current_password, :return_to) end end diff --git a/app/controllers/concerns/deprecation_concern.rb b/app/controllers/concerns/deprecation_concern.rb deleted file mode 100644 index ad8de724a1..0000000000 --- a/app/controllers/concerns/deprecation_concern.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module DeprecationConcern - extend ActiveSupport::Concern - - class_methods do - def deprecate_api(date, sunset: nil, **kwargs) - deprecation_timestamp = "@#{date.to_datetime.to_i}" - sunset = sunset&.to_date&.httpdate - - before_action(**kwargs) do - response.headers['Deprecation'] = deprecation_timestamp - response.headers['Sunset'] = sunset if sunset - end - end - end -end diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb index 14742e3b5c..ede299d5a4 100644 --- a/app/controllers/concerns/localized.rb +++ b/app/controllers/concerns/localized.rb @@ -25,7 +25,7 @@ module Localized end def available_locale_or_nil(locale_name) - locale_name.to_sym if locale_name.respond_to?(:to_sym) && I18n.available_locales.include?(locale_name.to_sym) + locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym) end def content_locale diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index ffe612f468..4ae63632c0 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -10,6 +10,8 @@ module SignatureVerification EXPIRATION_WINDOW_LIMIT = 12.hours CLOCK_SKEW_MARGIN = 1.hour + class SignatureVerificationError < StandardError; end + def require_account_signature! render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account end @@ -32,7 +34,7 @@ module SignatureVerification def signature_key_id signature_params['keyId'] - rescue Mastodon::SignatureVerificationError + rescue SignatureVerificationError nil end @@ -43,17 +45,17 @@ module SignatureVerification def signed_request_actor return @signed_request_actor if defined?(@signed_request_actor) - raise Mastodon::SignatureVerificationError, 'Request not signed' unless signed_request? - raise Mastodon::SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters? - raise Mastodon::SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm) - raise Mastodon::SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window? + raise SignatureVerificationError, 'Request not signed' unless signed_request? + raise SignatureVerificationError, 'Incompatible request signature. keyId and signature are required' if missing_required_signature_parameters? + raise SignatureVerificationError, 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)' unless %w(rsa-sha256 hs2019).include?(signature_algorithm) + raise SignatureVerificationError, 'Signed request date outside acceptable time window' unless matches_time_window? verify_signature_strength! verify_body_digest! actor = actor_from_key_id(signature_params['keyId']) - raise Mastodon::SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? + raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil? signature = Base64.decode64(signature_params['signature']) compare_signed_string = build_signed_string(include_query_string: true) @@ -66,7 +68,7 @@ module SignatureVerification actor = stoplight_wrapper.run { actor_refresh_key!(actor) } - raise Mastodon::SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? + raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil? compare_signed_string = build_signed_string(include_query_string: true) return actor unless verify_signature(actor, signature, compare_signed_string).nil? @@ -76,7 +78,7 @@ module SignatureVerification return actor unless verify_signature(actor, signature, compare_signed_string).nil? fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature'] - rescue Mastodon::SignatureVerificationError => e + rescue SignatureVerificationError => e fail_with! e.message rescue *Mastodon::HTTP_CONNECTION_ERRORS => e fail_with! "Failed to fetch remote data: #{e.message}" @@ -102,7 +104,7 @@ module SignatureVerification def signature_params @signature_params ||= SignatureParser.parse(request.headers['Signature']) rescue SignatureParser::ParsingError - raise Mastodon::SignatureVerificationError, 'Error parsing signature parameters' + raise SignatureVerificationError, 'Error parsing signature parameters' end def signature_algorithm @@ -114,31 +116,31 @@ module SignatureVerification end def verify_signature_strength! - raise Mastodon::SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)') - raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest') - raise Mastodon::SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host') - raise Mastodon::SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest') + raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)') + raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest') + raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host') + raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest') end def verify_body_digest! return unless signed_headers.include?('digest') - raise Mastodon::SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest') + raise SignatureVerificationError, 'Digest header missing' unless request.headers.key?('Digest') digests = request.headers['Digest'].split(',').map { |digest| digest.split('=', 2) }.map { |key, value| [key.downcase, value] } sha256 = digests.assoc('sha-256') - raise Mastodon::SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? + raise SignatureVerificationError, "Mastodon only supports SHA-256 in Digest header. Offered algorithms: #{digests.map(&:first).join(', ')}" if sha256.nil? return if body_digest == sha256[1] digest_size = begin Base64.strict_decode64(sha256[1].strip).length rescue ArgumentError - raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}" + raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a valid base64 string. Given digest: #{sha256[1]}" end - raise Mastodon::SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32 + raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32 - raise Mastodon::SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" + raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" end def verify_signature(actor, signature, compare_signed_string) @@ -153,23 +155,23 @@ module SignatureVerification def build_signed_string(include_query_string: true) signed_headers.map do |signed_header| case signed_header - when HttpSignatureDraft::REQUEST_TARGET + when Request::REQUEST_TARGET if include_query_string - "#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}" + "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}" else # Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header. # Therefore, temporarily support such incorrect signatures for compatibility. # TODO: remove eventually some time after release of the fixed version - "#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" + "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" end when '(created)' - raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' - raise Mastodon::SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? + 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']}" when '(expires)' - raise Mastodon::SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019' - raise Mastodon::SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? + 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? "(expires): #{signature_params['expires']}" else @@ -191,7 +193,7 @@ module SignatureVerification expires_time = Time.at(signature_params['expires'].to_i).utc if signature_params['expires'].present? rescue ArgumentError => e - raise Mastodon::SignatureVerificationError, "Invalid Date header: #{e.message}" + raise SignatureVerificationError, "Invalid Date header: #{e.message}" end expires_time ||= created_time + 5.minutes unless created_time.nil? @@ -231,9 +233,9 @@ module SignatureVerification account end rescue Mastodon::PrivateNetworkAddressError => e - raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, ActivityPub::FetchRemoteKeyService::Error, Webfinger::Error => e - raise Mastodon::SignatureVerificationError, e.message + raise SignatureVerificationError, e.message end def stoplight_wrapper @@ -249,8 +251,8 @@ module SignatureVerification ActivityPub::FetchRemoteActorService.new.call(actor.uri, only_key: true, suppress_errors: false) rescue Mastodon::PrivateNetworkAddressError => e - raise Mastodon::SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" + raise SignatureVerificationError, "Requests to private network addresses are disallowed (tried to query #{e.host})" rescue Mastodon::HostValidationError, ActivityPub::FetchRemoteActorService::Error, Webfinger::Error => e - raise Mastodon::SignatureVerificationError, e.message + raise SignatureVerificationError, e.message end end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index ec2256aa9c..249bb20a25 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -7,7 +7,6 @@ module WebAppControllerConcern vary_by 'Accept, Accept-Language, Cookie' before_action :redirect_unauthenticated_to_permalinks! - before_action :set_referer_header content_security_policy do |p| policy = ContentSecurityPolicy.new @@ -42,10 +41,4 @@ module WebAppControllerConcern end end end - - protected - - def set_referer_header - response.set_header('Referrer-Policy', Setting.allow_referrer_origin ? 'strict-origin-when-cross-origin' : 'same-origin') - end end diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 5b98914114..eb6417698a 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController + before_action :set_user_roles + def show - expires_in 1.month, public: true + expires_in 3.minutes, public: true render content_type: 'text/css' end @@ -12,4 +14,8 @@ class CustomCssController < ActionController::Base # rubocop:disable Rails/Appli Setting.custom_css end helper_method :custom_css_styles + + def set_user_roles + @user_roles = UserRole.providing_styles + end end diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb index 797f31cf78..98b58d2117 100644 --- a/app/controllers/disputes/appeals_controller.rb +++ b/app/controllers/disputes/appeals_controller.rb @@ -21,6 +21,6 @@ class Disputes::AppealsController < Disputes::BaseController end def appeal_params - params.expect(appeal: [:text]) + params.require(:appeal).permit(:text) end end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index 07677fd3f3..dd24a1b740 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -8,4 +8,11 @@ class Disputes::BaseController < ApplicationController skip_before_action :require_functional! before_action :authenticate_user! + before_action :set_cache_headers + + private + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index d85b017aaa..7ada13f680 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -6,6 +6,7 @@ class Filters::StatusesController < ApplicationController before_action :authenticate_user! before_action :set_filter before_action :set_status_filters + before_action :set_cache_headers PER_PAGE = 20 @@ -33,10 +34,14 @@ class Filters::StatusesController < ApplicationController end def status_filter_batch_action_params - params.expect(form_status_filter_batch_action: [status_filter_ids: []]) + params.require(:form_status_filter_batch_action).permit(status_filter_ids: []) end def action_from_button 'remove' if params[:remove] 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 20b8135908..7746db049f 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -5,6 +5,7 @@ class FiltersController < ApplicationController before_action :authenticate_user! before_action :set_filter, only: [:edit, :update, :destroy] + before_action :set_cache_headers def index @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @@ -47,6 +48,10 @@ class FiltersController < ApplicationController end def resource_params - params.expect(custom_filter: [:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [[:id, :keyword, :whole_word, :_destroy]]]) + params.require(:custom_filter).permit(:title, :expires_in, :filter_action, :exclude_follows, :exclude_localusers, :exclude_quote, :exclude_profile, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy]) + 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 85f6ccc5e4..44d90ec671 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -46,7 +46,7 @@ class FollowerAccountsController < ApplicationController end def page_url(page) - ActivityPub::TagManager.instance.followers_uri_for(@account, page: page) unless page.nil? + account_followers_url(@account, page: page) unless page.nil? end def next_page_url diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index fc65333ac4..070852695e 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -6,6 +6,7 @@ class InvitesController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_cache_headers def index authorize :invite, :create? @@ -42,6 +43,10 @@ class InvitesController < ApplicationController end def resource_params - params.expect(invite: [:max_uses, :expires_in, :autofollow, :comment]) + params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment) + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index deafedeaef..66e774425d 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -5,6 +5,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController before_action :store_current_location before_action :authenticate_resource_owner! + before_action :set_cache_headers content_security_policy do |p| p.form_action(false) @@ -31,4 +32,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController def truthy_param?(key) ActiveModel::Type::Boolean.new.cast(params[key]) end + + def set_cache_headers + 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 8b11a519ea..9e541e5e3c 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -6,6 +6,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! before_action :require_not_suspended!, only: :destroy + before_action :set_cache_headers before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } @@ -29,6 +30,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio forbidden if current_account.unavailable? 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 = current_resource_owner.applications_last_used end diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 7e793fc734..d351afcfb7 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -6,6 +6,7 @@ class RelationshipsController < ApplicationController before_action :authenticate_user! before_action :set_accounts, only: :show before_action :set_relationships, only: :show + before_action :set_cache_headers helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -35,7 +36,7 @@ class RelationshipsController < ApplicationController end def form_account_batch_params - params.expect(form_account_batch: [:action, account_ids: []]) + params.require(:form_account_batch).permit(:action, account_ids: []) end def following_relationship? @@ -65,4 +66,8 @@ class RelationshipsController < ApplicationController 'remove_domains_from_followers' end end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/settings/aliases_controller.rb b/app/controllers/settings/aliases_controller.rb index c21d43eeb3..a421b8ede3 100644 --- a/app/controllers/settings/aliases_controller.rb +++ b/app/controllers/settings/aliases_controller.rb @@ -30,7 +30,7 @@ class Settings::AliasesController < Settings::BaseController private def resource_params - params.expect(account_alias: [:acct]) + params.require(:account_alias).permit(:acct) end def set_alias diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 8e39741f89..d6573f9b49 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -2,6 +2,7 @@ class Settings::ApplicationsController < Settings::BaseController before_action :set_application, only: [:show, :update, :destroy, :regenerate] + before_action :prepare_scopes, only: [:create, :update] def index @applications = current_user.applications.order(id: :desc).page(params[:page]) @@ -59,6 +60,16 @@ class Settings::ApplicationsController < Settings::BaseController end def application_params - params.expect(doorkeeper_application: [:name, :redirect_uri, :website, scopes: []]) + params.require(:doorkeeper_application).permit( + :name, + :redirect_uri, + :scopes, + :website + ) + end + + def prepare_scopes + scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil) + params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array end end diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index 7f2279aa8f..188334ac23 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -4,9 +4,14 @@ class Settings::BaseController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_cache_headers private + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + def require_not_suspended! forbidden if current_account.unavailable? end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 815d95ad83..16c201b6b3 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -21,7 +21,7 @@ class Settings::DeletesController < Settings::BaseController private def resource_params - params.expect(form_delete_confirmation: [:password, :username]) + params.require(:form_delete_confirmation).permit(:password, :username) end def require_not_suspended! diff --git a/app/controllers/settings/featured_tags_controller.rb b/app/controllers/settings/featured_tags_controller.rb index 0f352e1913..7e29dd1d29 100644 --- a/app/controllers/settings/featured_tags_controller.rb +++ b/app/controllers/settings/featured_tags_controller.rb @@ -44,6 +44,6 @@ class Settings::FeaturedTagsController < Settings::BaseController end def featured_tag_params - params.expect(featured_tag: [:name]) + params.require(:featured_tag).permit(:name) end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index be1699315f..5346a448a3 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -90,7 +90,7 @@ class Settings::ImportsController < Settings::BaseController private def import_params - params.expect(form_import: [:data, :type, :mode]) + params.require(:form_import).permit(:data, :type, :mode) end def set_bulk_import diff --git a/app/controllers/settings/migration/redirects_controller.rb b/app/controllers/settings/migration/redirects_controller.rb index d850e05e94..6d469f3842 100644 --- a/app/controllers/settings/migration/redirects_controller.rb +++ b/app/controllers/settings/migration/redirects_controller.rb @@ -33,6 +33,6 @@ class Settings::Migration::RedirectsController < Settings::BaseController private def resource_params - params.expect(form_redirect: [:acct, :current_password, :current_username]) + params.require(:form_redirect).permit(:acct, :current_password, :current_username) end end diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index 92e3611fd9..62603aba81 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -27,7 +27,7 @@ class Settings::MigrationsController < Settings::BaseController private def resource_params - params.expect(account_migration: [:acct, :current_password, :current_username]) + params.require(:account_migration).permit(:acct, :current_password, :current_username) end def set_migrations diff --git a/app/controllers/settings/pictures_controller.rb b/app/controllers/settings/pictures_controller.rb index 7e61e6d580..58a4325307 100644 --- a/app/controllers/settings/pictures_controller.rb +++ b/app/controllers/settings/pictures_controller.rb @@ -8,7 +8,7 @@ module Settings def destroy if valid_picture? if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' }) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303 else redirect_to settings_profile_path diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb index c2e705da3c..d3e62fb5d9 100644 --- a/app/controllers/settings/preferences/base_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -25,10 +25,10 @@ class Settings::Preferences::BaseController < Settings::BaseController end def original_user_params - params.expect(user: [:locale, :time_zone, :custom_css_text, chosen_languages: [], settings_attributes: UserSettings.keys]) + params.require(:user).permit(:locale, :time_zone, :custom_css_text, chosen_languages: [], settings_attributes: UserSettings.keys) end def disabled_visibilities_params - params.expect(user: [settings_attributes: { enabled_visibilities: [] }]) + params.require(:user).permit(settings_attributes: { enabled_visibilities: [] }) end end diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb index 96efa03ccf..1102c89fad 100644 --- a/app/controllers/settings/privacy_controller.rb +++ b/app/controllers/settings/privacy_controller.rb @@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params.except(:settings)) current_user.update!(settings_attributes: account_params[:settings]) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg') else render :show @@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController private def account_params - params.expect(account: [:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys]) + params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys) end def set_account diff --git a/app/controllers/settings/privacy_extra_controller.rb b/app/controllers/settings/privacy_extra_controller.rb index f1292e644c..54cedf2c4b 100644 --- a/app/controllers/settings/privacy_extra_controller.rb +++ b/app/controllers/settings/privacy_extra_controller.rb @@ -18,7 +18,7 @@ class Settings::PrivacyExtraController < Settings::BaseController private def account_params - params.expect(account: [settings: UserSettings.keys]) + params.require(:account).permit(settings: UserSettings.keys) end def set_account diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 04a10fbfb9..dc759a060b 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg') else @account.build_fields @@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.expect(account: [:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, fields_attributes: [[:name, :value]]]) + params.require(:account).permit(:display_name, :note, :bio_markdown, :avatar, :header, :bot, :my_actor_type, fields_attributes: [:name, :value]) end def set_account diff --git a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb index eae990e79b..1a0afe58b0 100644 --- a/app/controllers/settings/two_factor_authentication/confirmations_controller.rb +++ b/app/controllers/settings/two_factor_authentication/confirmations_controller.rb @@ -38,7 +38,7 @@ module Settings private def confirmation_params - params.expect(form_two_factor_confirmation: [:otp_attempt]) + params.require(:form_two_factor_confirmation).permit(:otp_attempt) end def prepare_two_factor_form diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb index 4b949ca72d..4e0663253c 100644 --- a/app/controllers/settings/verifications_controller.rb +++ b/app/controllers/settings/verifications_controller.rb @@ -8,7 +8,7 @@ class Settings::VerificationsController < Settings::BaseController def update if UpdateAccountService.new.call(@account, account_params) - ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id) + ActivityPub::UpdateDistributionWorker.perform_async(@account.id) redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg') else render :show @@ -18,9 +18,7 @@ class Settings::VerificationsController < Settings::BaseController private def account_params - params.expect(account: [:attribution_domains]).tap do |params| - params[:attribution_domains] = params[:attribution_domains].split if params[:attribution_domains] - end + params.require(:account).permit(:attribution_domains_as_text) end def set_account diff --git a/app/controllers/severed_relationships_controller.rb b/app/controllers/severed_relationships_controller.rb index 817abebf62..965753a26f 100644 --- a/app/controllers/severed_relationships_controller.rb +++ b/app/controllers/severed_relationships_controller.rb @@ -4,6 +4,7 @@ class SeveredRelationshipsController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_cache_headers before_action :set_event, only: [:following, :followers] @@ -48,4 +49,8 @@ class SeveredRelationshipsController < ApplicationController def acct(account) account.local? ? account.local_username_and_domain : account.acct end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index a25e544392..4db02051cc 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -5,6 +5,7 @@ class StatusesCleanupController < ApplicationController before_action :authenticate_user! before_action :set_policy + before_action :set_cache_headers def show; end @@ -14,6 +15,8 @@ class StatusesCleanupController < ApplicationController else render :show end + rescue ActionController::ParameterMissing + # Do nothing end def require_functional! @@ -27,6 +30,10 @@ class StatusesCleanupController < ApplicationController end def resource_params - params.expect(account_statuses_cleanup_policy: [:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis]) + params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :keep_self_emoji, :min_favs, :min_reblogs, :min_emojis) + end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/system_css_controller.rb b/app/controllers/system_css_controller.rb index dd90491894..a19728bbfd 100644 --- a/app/controllers/system_css_controller.rb +++ b/app/controllers/system_css_controller.rb @@ -1,8 +1,16 @@ # frozen_string_literal: true class SystemCssController < ActionController::Base # rubocop:disable Rails/ApplicationController + before_action :set_user_roles + def show expires_in 3.minutes, public: true render content_type: 'text/css' end + + private + + def set_user_roles + @user_roles = UserRole.providing_styles + end end diff --git a/app/controllers/terms_of_service_controller.rb b/app/controllers/terms_of_service_controller.rb deleted file mode 100644 index 672fb07915..0000000000 --- a/app/controllers/terms_of_service_controller.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class TermsOfServiceController < ApplicationController - include WebAppControllerConcern - - skip_before_action :require_functional! - - def show - expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? - end -end diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index 33da1f7216..c7a59660cf 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -2,18 +2,11 @@ module Admin::Trends::StatusesHelper def one_line_preview(status) - text = begin - if status.local? - status.text.split("\n").first - else - Nokogiri::HTML5(status.text).css('html > body > *').first&.text - end - rescue ArgumentError - # This can happen if one of the Nokogumbo limits is encountered - # Unfortunately, it does not use a more precise error class - # nor allows more graceful handling - '' - end + text = if status.local? + status.text.split("\n").first + else + Nokogiri::HTML5(status.text).css('html > body > *').first&.text + end return '' if text.blank? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0d7d0e8117..54007704f9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -149,7 +149,6 @@ module ApplicationHelper output << content_for(:body_classes) output << "theme-#{current_theme.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui - output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion') output << 'rtl' if locale_direction == 'rtl' output << "content-font-size__#{current_account&.user&.setting_content_font_size}" @@ -263,14 +262,6 @@ module ApplicationHelper I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people end - def app_store_url_ios - 'https://apps.apple.com/app/mastodon-for-iphone-and-ipad/id1571998974' - end - - def app_store_url_android - 'https://play.google.com/store/apps/details?id=org.joinmastodon.android' - end - private def storage_host_var diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 077c5272a5..03ca88670f 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -34,7 +34,6 @@ module ContextHelper license: { 'schema' => 'http://schema.org#', 'license' => 'schema:license' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } }, - misskey_license: { 'misskey' => 'https://misskey-hub.net/ns#', '_misskey_license' => 'misskey:_misskey_license' }, }.freeze def full_context diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index dc7442ac33..8ba2d7acd3 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -68,10 +68,6 @@ module FormattingHelper end end - def markdown(text) - Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true).render(text).html_safe # rubocop:disable Rails/OutputSafety - end - private def wrapped_status_content_format(status) diff --git a/app/helpers/json_ld_helper.rb b/app/helpers/jsonld_helper.rb similarity index 84% rename from app/helpers/json_ld_helper.rb rename to app/helpers/jsonld_helper.rb index 693cdf730f..2a5c2d8826 100644 --- a/app/helpers/json_ld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -163,49 +163,24 @@ module JsonLdHelper end end - # Fetch the resource given by uri. - # @param uri [String] - # @param id_is_known [Boolean] - # @param on_behalf_of [nil, Account] - # @param raise_on_error [Symbol<:all, :temporary, :none>] See {#fetch_resource_without_id_validation} for possible values - def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: :none, request_options: {}) + def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {}) unless id_is_known - json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error) + json = fetch_resource_without_id_validation(uri, on_behalf_of) return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id']) uri = json['id'] end - json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error, request_options: request_options) + json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options) json.present? && json['id'] == uri ? json : nil end - # Fetch the resource given by uri - # - # If an error is raised, it contains the response and can be captured for handling like - # - # begin - # fetch_resource_without_id_validation(uri, nil, true) - # rescue Mastodon::UnexpectedResponseError => e - # e.response - # end - # - # @param uri [String] - # @param on_behalf_of [nil, Account] - # @param raise_on_error [Symbol<:all, :temporary, :none>] - # - +:all+ - raise if response code is not in the 2xx range - # - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404 - # (see {#response_error_unsalvageable} ) - # - +:none+ - do not raise, return +nil+ - def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: :none, request_options: {}) + def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {}) on_behalf_of ||= Account.representative build_request(uri, on_behalf_of, options: request_options).perform do |response| - raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && ( - raise_on_error == :all || - (!response_error_unsalvageable?(response) && raise_on_error == :temporary) - ) + raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response) end diff --git a/app/helpers/kmyblue_capabilities_helper.rb b/app/helpers/kmyblue_capabilities_helper.rb index 279505bec8..075869ee60 100644 --- a/app/helpers/kmyblue_capabilities_helper.rb +++ b/app/helpers/kmyblue_capabilities_helper.rb @@ -20,8 +20,6 @@ module KmyblueCapabilitiesHelper kmyblue_circle_history kmyblue_list_notification kmyblue_server_features - favourite_list - kmyblue_favourite_antenna ) capabilities << :full_text_search if Chewy.enabled? diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index f4d88a1ef0..fab899a533 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -23,51 +23,8 @@ module ThemeHelper end end - def custom_stylesheet - if active_custom_stylesheet.present? - stylesheet_link_tag( - custom_css_path(active_custom_stylesheet), - host: root_url, - media: :all, - skip_pipeline: true - ) - end - end - - def system_stylesheet - stylesheet_link_tag( - system_css_path, - host: root_url, - media: :all, - skip_pipeline: true - ) - end - - def user_custom_stylesheet - stylesheet_link_tag( - user_custom_css_path({ version: user_custom_css_version }), - host: root_url, - media: :all, - skip_pipeline: true - ) - end - private - def active_custom_stylesheet - if cached_custom_css_digest.present? - [:custom, cached_custom_css_digest.to_s.first(8)] - .compact_blank - .join('-') - end - end - - def cached_custom_css_digest - Rails.cache.fetch(:setting_digest_custom_css) do - Setting.custom_css&.then { |content| Digest::SHA256.hexdigest(content) } - end - end - def theme_color_for(theme) theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark] end diff --git a/app/inputs/date_of_birth_input.rb b/app/inputs/date_of_birth_input.rb deleted file mode 100644 index 131234b02e..0000000000 --- a/app/inputs/date_of_birth_input.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class DateOfBirthInput < SimpleForm::Inputs::Base - OPTIONS = [ - { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze, - { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze, - { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze, - ].freeze - - def input(wrapper_options = nil) - merged_input_options = merge_wrapper_options(input_html_options, wrapper_options) - merged_input_options[:inputmode] = 'numeric' - - values = (object.public_send(attribute_name) || '').split('.') - - safe_join(Array.new(3) do |index| - options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index] - @builder.text_field("#{attribute_name}(#{index + 1}i)", options) - end) - end - - def label_target - "#{attribute_name}_1i" - end - - private - - def generate_id(index) - "#{object_name}_#{attribute_name}_#{index + 1}i" - end -end diff --git a/app/javascript/entrypoints/embed.tsx b/app/javascript/entrypoints/embed.tsx index 6c091e4d07..f8c824d287 100644 --- a/app/javascript/entrypoints/embed.tsx +++ b/app/javascript/entrypoints/embed.tsx @@ -1,7 +1,7 @@ import './public-path'; import { createRoot } from 'react-dom/client'; -import { afterInitialRender } from 'mastodon/hooks/useRenderSignal'; +import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal'; import { start } from '../mastodon/common'; import { Status } from '../mastodon/features/standalone/status'; @@ -60,10 +60,6 @@ window.addEventListener('message', (e) => { const data = e.data; - // Only set overflow to `hidden` once we got the expected `message` so the post can still be scrolled if - // embedded without parent Javascript support - document.body.style.overflow = 'hidden'; - // We use a timeout to allow for the React page to render before calculating the height afterInitialRender(() => { window.parent.postMessage( diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index 9374d6b2d1..c1e8418014 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -68,7 +68,7 @@ function loaded() { if (id) message = localeData[id]; - message ??= defaultMessage as string; + if (!message) message = defaultMessage as string; const messageFormat = new IntlMessageFormat(message, locale); return messageFormat.format(values) as string; @@ -119,11 +119,7 @@ function loaded() { formattedContent = dateFormat.format(datetime); } - const timeGiven = content.dateTime.includes('T'); - content.title = timeGiven - ? dateTimeFormat.format(datetime) - : dateFormat.format(datetime); - + content.title = formattedContent; content.textContent = formattedContent; }); @@ -234,6 +230,62 @@ function loaded() { } }, ); + + Rails.delegate( + document, + 'button.status__content__spoiler-link', + 'click', + function () { + if (!(this instanceof HTMLButtonElement)) return; + + const statusEl = this.parentNode?.parentNode; + + if ( + !( + statusEl instanceof HTMLDivElement && + statusEl.classList.contains('.status__content') + ) + ) + return; + + if (statusEl.dataset.spoiler === 'expanded') { + statusEl.dataset.spoiler = 'folded'; + this.textContent = new IntlMessageFormat( + localeData['status.show_more'] ?? 'Show more', + locale, + ).format() as string; + } else { + statusEl.dataset.spoiler = 'expanded'; + this.textContent = new IntlMessageFormat( + localeData['status.show_less'] ?? 'Show less', + locale, + ).format() as string; + } + }, + ); + + document + .querySelectorAll('button.status__content__spoiler-link') + .forEach((spoilerLink) => { + const statusEl = spoilerLink.parentNode?.parentNode; + + if ( + !( + statusEl instanceof HTMLDivElement && + statusEl.classList.contains('.status__content') + ) + ) + return; + + const message = + statusEl.dataset.spoiler === 'expanded' + ? (localeData['status.show_less'] ?? 'Show less') + : (localeData['status.show_more'] ?? 'Show more'); + spoilerLink.textContent = new IntlMessageFormat( + message, + locale, + ).format() as string; + }); } Rails.delegate( @@ -387,24 +439,6 @@ Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => { }); }); -Rails.delegate(document, '.rules-list button', 'click', ({ target }) => { - if (!(target instanceof HTMLElement)) { - return; - } - - const button = target.closest('button'); - - if (!button) { - return; - } - - if (button.ariaExpanded === 'true') { - button.ariaExpanded = 'false'; - } else { - button.ariaExpanded = 'true'; - } -}); - function main() { ready(loaded).catch((error: unknown) => { console.error(error); diff --git a/app/javascript/mastodon/hooks/useHovering.ts b/app/javascript/hooks/useHovering.ts similarity index 100% rename from app/javascript/mastodon/hooks/useHovering.ts rename to app/javascript/hooks/useHovering.ts diff --git a/app/javascript/mastodon/hooks/useLinks.ts b/app/javascript/hooks/useLinks.ts similarity index 53% rename from app/javascript/mastodon/hooks/useLinks.ts rename to app/javascript/hooks/useLinks.ts index abaa108f6b..f08b9500da 100644 --- a/app/javascript/mastodon/hooks/useLinks.ts +++ b/app/javascript/hooks/useLinks.ts @@ -2,8 +2,6 @@ import { useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { isFulfilled, isRejected } from '@reduxjs/toolkit'; - import { openURL } from 'mastodon/actions/search'; import { useAppDispatch } from 'mastodon/store'; @@ -14,9 +12,6 @@ const isHashtagClick = (element: HTMLAnchorElement) => element.textContent?.[0] === '#' || element.previousSibling?.textContent?.endsWith('#'); -const isFeaturedHashtagClick = (element: HTMLAnchorElement) => - isHashtagClick(element) && element.href.includes('/tagged/'); - export const useLinks = () => { const history = useHistory(); const dispatch = useAppDispatch(); @@ -32,36 +27,13 @@ export const useLinks = () => { [history], ); - const handleFeaturedHashtagClick = useCallback( - (element: HTMLAnchorElement) => { - const { textContent, href } = element; - - if (!textContent) return; - - const url = new URL(href); - - history.push(url.pathname); - }, - [history], - ); - const handleMentionClick = useCallback( - async (element: HTMLAnchorElement) => { - const result = await dispatch(openURL({ url: element.href })); - - if (isFulfilled(result)) { - if (result.payload.accounts[0]) { - history.push(`/@${result.payload.accounts[0].acct}`); - } else if (result.payload.statuses[0]) { - history.push( - `/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`, - ); - } else { + (element: HTMLAnchorElement) => { + dispatch( + openURL(element.href, history, () => { window.location.href = element.href; - } - } else if (isRejected(result)) { - window.location.href = element.href; - } + }), + ); }, [dispatch, history], ); @@ -76,16 +48,13 @@ export const useLinks = () => { if (isMentionClick(target)) { e.preventDefault(); - void handleMentionClick(target); - } else if (isFeaturedHashtagClick(target)) { - e.preventDefault(); - handleFeaturedHashtagClick(target); + handleMentionClick(target); } else if (isHashtagClick(target)) { e.preventDefault(); handleHashtagClick(target); } }, - [handleMentionClick, handleFeaturedHashtagClick, handleHashtagClick], + [handleMentionClick, handleHashtagClick], ); return handleClick; diff --git a/app/javascript/mastodon/hooks/useRenderSignal.ts b/app/javascript/hooks/useRenderSignal.ts similarity index 100% rename from app/javascript/mastodon/hooks/useRenderSignal.ts rename to app/javascript/hooks/useRenderSignal.ts diff --git a/app/javascript/mastodon/hooks/useSearchParam.ts b/app/javascript/hooks/useSearchParam.ts similarity index 100% rename from app/javascript/mastodon/hooks/useSearchParam.ts rename to app/javascript/hooks/useSearchParam.ts diff --git a/app/javascript/mastodon/hooks/useTimeout.ts b/app/javascript/hooks/useTimeout.ts similarity index 100% rename from app/javascript/mastodon/hooks/useTimeout.ts rename to app/javascript/hooks/useTimeout.ts diff --git a/app/javascript/icons/android-chrome-144x144.png b/app/javascript/icons/android-chrome-144x144.png old mode 100644 new mode 100755 index 698fb4a260..d636e94c43 Binary files a/app/javascript/icons/android-chrome-144x144.png and b/app/javascript/icons/android-chrome-144x144.png differ diff --git a/app/javascript/icons/android-chrome-192x192.png b/app/javascript/icons/android-chrome-192x192.png old mode 100644 new mode 100755 index 2b6b632648..4a2681ffb9 Binary files a/app/javascript/icons/android-chrome-192x192.png and b/app/javascript/icons/android-chrome-192x192.png differ diff --git a/app/javascript/icons/android-chrome-256x256.png b/app/javascript/icons/android-chrome-256x256.png old mode 100644 new mode 100755 index 51e3849a26..8fab493ede Binary files a/app/javascript/icons/android-chrome-256x256.png and b/app/javascript/icons/android-chrome-256x256.png differ diff --git a/app/javascript/icons/android-chrome-36x36.png b/app/javascript/icons/android-chrome-36x36.png old mode 100644 new mode 100755 index 925f69c4fc..335d012db1 Binary files a/app/javascript/icons/android-chrome-36x36.png and b/app/javascript/icons/android-chrome-36x36.png differ diff --git a/app/javascript/icons/android-chrome-384x384.png b/app/javascript/icons/android-chrome-384x384.png old mode 100644 new mode 100755 index 9d256a83cb..02b1e6fced Binary files a/app/javascript/icons/android-chrome-384x384.png and b/app/javascript/icons/android-chrome-384x384.png differ diff --git a/app/javascript/icons/android-chrome-48x48.png b/app/javascript/icons/android-chrome-48x48.png old mode 100644 new mode 100755 index bcfe7475d0..43cf411b8c Binary files a/app/javascript/icons/android-chrome-48x48.png and b/app/javascript/icons/android-chrome-48x48.png differ diff --git a/app/javascript/icons/android-chrome-512x512.png b/app/javascript/icons/android-chrome-512x512.png old mode 100644 new mode 100755 index bffacfb699..1856b80c7c Binary files a/app/javascript/icons/android-chrome-512x512.png and b/app/javascript/icons/android-chrome-512x512.png differ diff --git a/app/javascript/icons/android-chrome-72x72.png b/app/javascript/icons/android-chrome-72x72.png old mode 100644 new mode 100755 index 16679d5731..335008bf85 Binary files a/app/javascript/icons/android-chrome-72x72.png and b/app/javascript/icons/android-chrome-72x72.png differ diff --git a/app/javascript/icons/android-chrome-96x96.png b/app/javascript/icons/android-chrome-96x96.png old mode 100644 new mode 100755 index 9ade87cf32..d1cb095822 Binary files a/app/javascript/icons/android-chrome-96x96.png and b/app/javascript/icons/android-chrome-96x96.png differ diff --git a/app/javascript/icons/apple-touch-icon-1024x1024.png b/app/javascript/icons/apple-touch-icon-1024x1024.png old mode 100644 new mode 100755 index 8ec371eb27..c2a2d516ef Binary files a/app/javascript/icons/apple-touch-icon-1024x1024.png and b/app/javascript/icons/apple-touch-icon-1024x1024.png differ diff --git a/app/javascript/icons/apple-touch-icon-114x114.png b/app/javascript/icons/apple-touch-icon-114x114.png old mode 100644 new mode 100755 index e1563f51e5..218b415439 Binary files a/app/javascript/icons/apple-touch-icon-114x114.png and b/app/javascript/icons/apple-touch-icon-114x114.png differ diff --git a/app/javascript/icons/apple-touch-icon-120x120.png b/app/javascript/icons/apple-touch-icon-120x120.png old mode 100644 new mode 100755 index e9a5f5b0e5..be53bc7c10 Binary files a/app/javascript/icons/apple-touch-icon-120x120.png and b/app/javascript/icons/apple-touch-icon-120x120.png differ diff --git a/app/javascript/icons/apple-touch-icon-144x144.png b/app/javascript/icons/apple-touch-icon-144x144.png old mode 100644 new mode 100755 index 698fb4a260..cbb055732f Binary files a/app/javascript/icons/apple-touch-icon-144x144.png and b/app/javascript/icons/apple-touch-icon-144x144.png differ diff --git a/app/javascript/icons/apple-touch-icon-152x152.png b/app/javascript/icons/apple-touch-icon-152x152.png old mode 100644 new mode 100755 index 0cc93cc288..3a7975c054 Binary files a/app/javascript/icons/apple-touch-icon-152x152.png and b/app/javascript/icons/apple-touch-icon-152x152.png differ diff --git a/app/javascript/icons/apple-touch-icon-167x167.png b/app/javascript/icons/apple-touch-icon-167x167.png old mode 100644 new mode 100755 index 9bbbf53120..25be4eb5f5 Binary files a/app/javascript/icons/apple-touch-icon-167x167.png and b/app/javascript/icons/apple-touch-icon-167x167.png differ diff --git a/app/javascript/icons/apple-touch-icon-180x180.png b/app/javascript/icons/apple-touch-icon-180x180.png old mode 100644 new mode 100755 index 329b803b91..dc0e9bc20b Binary files a/app/javascript/icons/apple-touch-icon-180x180.png and b/app/javascript/icons/apple-touch-icon-180x180.png differ diff --git a/app/javascript/icons/apple-touch-icon-192x192.png b/app/javascript/icons/apple-touch-icon-192x192.png deleted file mode 100644 index 2b6b632648..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-192x192.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-256x256.png b/app/javascript/icons/apple-touch-icon-256x256.png deleted file mode 100644 index 51e3849a26..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-256x256.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-36x36.png b/app/javascript/icons/apple-touch-icon-36x36.png deleted file mode 100644 index 925f69c4fc..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-36x36.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-384x384.png b/app/javascript/icons/apple-touch-icon-384x384.png deleted file mode 100644 index 9d256a83cb..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-384x384.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-48x48.png b/app/javascript/icons/apple-touch-icon-48x48.png deleted file mode 100644 index bcfe7475d0..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-48x48.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-512x512.png b/app/javascript/icons/apple-touch-icon-512x512.png deleted file mode 100644 index bffacfb699..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-512x512.png and /dev/null differ diff --git a/app/javascript/icons/apple-touch-icon-57x57.png b/app/javascript/icons/apple-touch-icon-57x57.png old mode 100644 new mode 100755 index e00e142c64..bb0dc957cd Binary files a/app/javascript/icons/apple-touch-icon-57x57.png and b/app/javascript/icons/apple-touch-icon-57x57.png differ diff --git a/app/javascript/icons/apple-touch-icon-60x60.png b/app/javascript/icons/apple-touch-icon-60x60.png old mode 100644 new mode 100755 index 011285b564..9143a0bf07 Binary files a/app/javascript/icons/apple-touch-icon-60x60.png and b/app/javascript/icons/apple-touch-icon-60x60.png differ diff --git a/app/javascript/icons/apple-touch-icon-72x72.png b/app/javascript/icons/apple-touch-icon-72x72.png old mode 100644 new mode 100755 index 16679d5731..2b7d19484c Binary files a/app/javascript/icons/apple-touch-icon-72x72.png and b/app/javascript/icons/apple-touch-icon-72x72.png differ diff --git a/app/javascript/icons/apple-touch-icon-76x76.png b/app/javascript/icons/apple-touch-icon-76x76.png old mode 100644 new mode 100755 index 83c8748876..0985e33bcb Binary files a/app/javascript/icons/apple-touch-icon-76x76.png and b/app/javascript/icons/apple-touch-icon-76x76.png differ diff --git a/app/javascript/icons/apple-touch-icon-96x96.png b/app/javascript/icons/apple-touch-icon-96x96.png deleted file mode 100644 index 9ade87cf32..0000000000 Binary files a/app/javascript/icons/apple-touch-icon-96x96.png and /dev/null differ diff --git a/app/javascript/icons/favicon-16x16.png b/app/javascript/icons/favicon-16x16.png old mode 100644 new mode 100755 index 7f865cfe96..1326ba0462 Binary files a/app/javascript/icons/favicon-16x16.png and b/app/javascript/icons/favicon-16x16.png differ diff --git a/app/javascript/icons/favicon-32x32.png b/app/javascript/icons/favicon-32x32.png old mode 100644 new mode 100755 index 7f865cfe96..f5058cb0a5 Binary files a/app/javascript/icons/favicon-32x32.png and b/app/javascript/icons/favicon-32x32.png differ diff --git a/app/javascript/icons/favicon-48x48.png b/app/javascript/icons/favicon-48x48.png old mode 100644 new mode 100755 index 7f865cfe96..6253d054c7 Binary files a/app/javascript/icons/favicon-48x48.png and b/app/javascript/icons/favicon-48x48.png differ diff --git a/app/javascript/images/archetypes/booster.png b/app/javascript/images/archetypes/booster.png index df2a0226f8..18c92dfb7d 100755 Binary files a/app/javascript/images/archetypes/booster.png and b/app/javascript/images/archetypes/booster.png differ diff --git a/app/javascript/images/archetypes/lurker.png b/app/javascript/images/archetypes/lurker.png index e37f98aab2..8e1d6451b0 100755 Binary files a/app/javascript/images/archetypes/lurker.png and b/app/javascript/images/archetypes/lurker.png differ diff --git a/app/javascript/images/archetypes/oracle.png b/app/javascript/images/archetypes/oracle.png index 9d4e2177c5..2afd3c72e1 100755 Binary files a/app/javascript/images/archetypes/oracle.png and b/app/javascript/images/archetypes/oracle.png differ diff --git a/app/javascript/images/archetypes/pollster.png b/app/javascript/images/archetypes/pollster.png index 9fe6281af0..b838fccdd6 100755 Binary files a/app/javascript/images/archetypes/pollster.png and b/app/javascript/images/archetypes/pollster.png differ diff --git a/app/javascript/images/archetypes/replier.png b/app/javascript/images/archetypes/replier.png index 6c6325b9f1..b298d4221c 100755 Binary files a/app/javascript/images/archetypes/replier.png and b/app/javascript/images/archetypes/replier.png differ diff --git a/app/javascript/images/reticle.png b/app/javascript/images/reticle.png new file mode 100644 index 0000000000..a724ac0bcd Binary files /dev/null and b/app/javascript/images/reticle.png differ diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index d821381ce0..3d0e8b8c90 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -142,13 +142,6 @@ export function fetchAccountFail(id, error) { }; } -/** - * @param {string} id - * @param {Object} options - * @param {boolean} [options.reblogs] - * @param {boolean} [options.notify] - * @returns {function(): void} - */ export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { const alreadyFollowing = getState().getIn(['relationships', id, 'following']); diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js new file mode 100644 index 0000000000..48dee2587f --- /dev/null +++ b/app/javascript/mastodon/actions/alerts.js @@ -0,0 +1,66 @@ +import { defineMessages } from 'react-intl'; + +import { AxiosError } from 'axios'; + +const messages = defineMessages({ + unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, + unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' }, + rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' }, + rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' }, +}); + +export const ALERT_SHOW = 'ALERT_SHOW'; +export const ALERT_DISMISS = 'ALERT_DISMISS'; +export const ALERT_CLEAR = 'ALERT_CLEAR'; +export const ALERT_NOOP = 'ALERT_NOOP'; + +export const dismissAlert = alert => ({ + type: ALERT_DISMISS, + alert, +}); + +export const clearAlert = () => ({ + type: ALERT_CLEAR, +}); + +export const showAlert = alert => ({ + type: ALERT_SHOW, + alert, +}); + +export const showAlertForError = (error, skipNotFound = false) => { + if (error.response) { + const { data, status, statusText, headers } = error.response; + + // Skip these errors as they are reflected in the UI + if (skipNotFound && (status === 404 || status === 410)) { + return { type: ALERT_NOOP }; + } + + // Rate limit errors + if (status === 429 && headers['x-ratelimit-reset']) { + return showAlert({ + title: messages.rateLimitedTitle, + message: messages.rateLimitedMessage, + values: { 'retry_time': new Date(headers['x-ratelimit-reset']) }, + }); + } + + return showAlert({ + title: `${status}`, + message: data.error || statusText, + }); + } + + // An aborted request, e.g. due to reloading the browser window, it not really error + if (error.code === AxiosError.ECONNABORTED) { + return { type: ALERT_NOOP }; + } + + console.error(error); + + return showAlert({ + title: messages.unexpectedTitle, + message: messages.unexpectedMessage, + }); +}; diff --git a/app/javascript/mastodon/actions/alerts.ts b/app/javascript/mastodon/actions/alerts.ts deleted file mode 100644 index 4fd293e252..0000000000 --- a/app/javascript/mastodon/actions/alerts.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { defineMessages } from 'react-intl'; - -import { createAction } from '@reduxjs/toolkit'; - -import { AxiosError } from 'axios'; -import type { AxiosResponse } from 'axios'; - -import type { Alert } from 'mastodon/models/alert'; - -interface ApiErrorResponse { - error?: string; -} - -const messages = defineMessages({ - unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, - unexpectedMessage: { - id: 'alert.unexpected.message', - defaultMessage: 'An unexpected error occurred.', - }, - rateLimitedTitle: { - id: 'alert.rate_limited.title', - defaultMessage: 'Rate limited', - }, - rateLimitedMessage: { - id: 'alert.rate_limited.message', - defaultMessage: 'Please retry after {retry_time, time, medium}.', - }, -}); - -export const dismissAlert = createAction<{ key: number }>('alerts/dismiss'); - -export const clearAlerts = createAction('alerts/clear'); - -export const showAlert = createAction>('alerts/show'); - -const ignoreAlert = createAction('alerts/ignore'); - -export const showAlertForError = (error: unknown, skipNotFound = false) => { - if (error instanceof AxiosError && error.response) { - const { status, statusText, headers } = error.response; - const { data } = error.response as AxiosResponse; - - // Skip these errors as they are reflected in the UI - if (skipNotFound && (status === 404 || status === 410)) { - return ignoreAlert(); - } - - // Rate limit errors - if (status === 429 && headers['x-ratelimit-reset']) { - return showAlert({ - title: messages.rateLimitedTitle, - message: messages.rateLimitedMessage, - values: { - retry_time: new Date(headers['x-ratelimit-reset'] as string), - }, - }); - } - - return showAlert({ - title: `${status}`, - message: data.error ?? statusText, - }); - } - - // An aborted request, e.g. due to reloading the browser window, is not really an error - if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) { - return ignoreAlert(); - } - - console.error(error); - - return showAlert({ - title: messages.unexpectedTitle, - message: messages.unexpectedMessage, - }); -}; diff --git a/app/javascript/mastodon/actions/bookmark_categories.js b/app/javascript/mastodon/actions/bookmark_categories.js index 313d5de8f2..eab632ab7a 100644 --- a/app/javascript/mastodon/actions/bookmark_categories.js +++ b/app/javascript/mastodon/actions/bookmark_categories.js @@ -129,9 +129,9 @@ export const fetchBookmarkCategoryStatusesFail = (id, error) => ({ export function expandBookmarkCategoryStatuses(bookmarkCategoryId) { return (dispatch, getState) => { - const url = getState().getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'next'], null); + const url = getState().getIn(['bookmark_categories', bookmarkCategoryId, 'next'], null); - if (url === null || getState().getIn(['status_lists', 'bookmark_category_statuses', bookmarkCategoryId, 'isLoading'])) { + if (url === null || getState().getIn(['bookmark_categories', bookmarkCategoryId, 'isLoading'])) { return; } diff --git a/app/javascript/mastodon/actions/circles.js b/app/javascript/mastodon/actions/circles.js index 221c0c683a..c9cbb2cd13 100644 --- a/app/javascript/mastodon/actions/circles.js +++ b/app/javascript/mastodon/actions/circles.js @@ -152,9 +152,9 @@ export function fetchCircleStatusesFail(id, error) { export function expandCircleStatuses(circleId) { return (dispatch, getState) => { - const url = getState().getIn(['status_lists', 'circle_statuses', circleId, 'next'], null); + const url = getState().getIn(['circles', circleId, 'next'], null); - if (url === null || getState().getIn(['status_lists', 'circle_statuses', circleId, 'isLoading'])) { + if (url === null || getState().getIn(['circles', circleId, 'isLoading'])) { return; } diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 9a92528f3a..6059fe0e7e 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -441,7 +441,7 @@ export function initMediaEditModal(id) { dispatch(openModal({ modalType: 'FOCAL_POINT', - modalProps: { mediaId: id }, + modalProps: { id }, })); }; } diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts deleted file mode 100644 index 97f0d68c51..0000000000 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; - -import { apiUpdateMedia } from 'mastodon/api/compose'; -import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; -import type { MediaAttachment } from 'mastodon/models/media_attachment'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { - unattached?: boolean; -}; - -const simulateModifiedApiResponse = ( - media: MediaAttachment, - params: { description?: string; focus?: string }, -): SimulatedMediaAttachmentJSON => { - const [x, y] = (params.focus ?? '').split(','); - - const data = { - ...media.toJS(), - ...params, - meta: { - focus: { - x: parseFloat(x ?? '0'), - y: parseFloat(y ?? '0'), - }, - }, - } as unknown as SimulatedMediaAttachmentJSON; - - return data; -}; - -export const changeUploadCompose = createDataLoadingThunk( - 'compose/changeUpload', - async ( - { - id, - ...params - }: { - id: string; - description: string; - focus: string; - }, - { getState }, - ) => { - const media = ( - (getState().compose as ImmutableMap).get( - 'media_attachments', - ) as ImmutableList - ).find((item) => item.get('id') === id); - - // Editing already-attached media is deferred to editing the post itself. - // For simplicity's sake, fake an API reply. - if (media && !media.get('unattached')) { - return new Promise((resolve) => { - resolve(simulateModifiedApiResponse(media, params)); - }); - } - - return apiUpdateMedia(id, params); - }, - (media: SimulatedMediaAttachmentJSON) => { - return { - media, - attached: typeof media.unattached !== 'undefined' && !media.unattached, - }; - }, - { - useLoadingBar: false, - }, -); diff --git a/app/javascript/mastodon/actions/domain_blocks.js b/app/javascript/mastodon/actions/domain_blocks.js index 279ec1bef7..727f800af3 100644 --- a/app/javascript/mastodon/actions/domain_blocks.js +++ b/app/javascript/mastodon/actions/domain_blocks.js @@ -12,6 +12,14 @@ export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; +export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; +export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; +export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; + +export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST'; +export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS'; +export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL'; + export function blockDomain(domain) { return (dispatch, getState) => { dispatch(blockDomainRequest(domain)); @@ -71,6 +79,80 @@ export function unblockDomainFail(domain, error) { }; } +export function fetchDomainBlocks() { + return (dispatch) => { + dispatch(fetchDomainBlocksRequest()); + + api().get('/api/v1/domain_blocks').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(fetchDomainBlocksFail(err)); + }); + }; +} + +export function fetchDomainBlocksRequest() { + return { + type: DOMAIN_BLOCKS_FETCH_REQUEST, + }; +} + +export function fetchDomainBlocksSuccess(domains, next) { + return { + type: DOMAIN_BLOCKS_FETCH_SUCCESS, + domains, + next, + }; +} + +export function fetchDomainBlocksFail(error) { + return { + type: DOMAIN_BLOCKS_FETCH_FAIL, + error, + }; +} + +export function expandDomainBlocks() { + return (dispatch, getState) => { + const url = getState().getIn(['domain_lists', 'blocks', 'next']); + + if (!url) { + return; + } + + dispatch(expandDomainBlocksRequest()); + + api().get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(expandDomainBlocksFail(err)); + }); + }; +} + +export function expandDomainBlocksRequest() { + return { + type: DOMAIN_BLOCKS_EXPAND_REQUEST, + }; +} + +export function expandDomainBlocksSuccess(domains, next) { + return { + type: DOMAIN_BLOCKS_EXPAND_SUCCESS, + domains, + next, + }; +} + +export function expandDomainBlocksFail(error) { + return { + type: DOMAIN_BLOCKS_EXPAND_FAIL, + error, + }; +} + export const initDomainBlockModal = account => dispatch => dispatch(openModal({ modalType: 'DOMAIN_BLOCK', modalProps: { diff --git a/app/javascript/mastodon/actions/dropdown_menu.ts b/app/javascript/mastodon/actions/dropdown_menu.ts index d9d395ba33..3694df1ae0 100644 --- a/app/javascript/mastodon/actions/dropdown_menu.ts +++ b/app/javascript/mastodon/actions/dropdown_menu.ts @@ -1,11 +1,11 @@ import { createAction } from '@reduxjs/toolkit'; export const openDropdownMenu = createAction<{ - id: number; + id: string; keyboard: boolean; - scrollKey?: string; + scrollKey: string; }>('dropdownMenu/open'); -export const closeDropdownMenu = createAction<{ id: number }>( +export const closeDropdownMenu = createAction<{ id: string }>( 'dropdownMenu/close', ); diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index fc165b1a1f..ebf58b761a 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -1,12 +1,10 @@ -import { createPollFromServerJSON } from 'mastodon/models/poll'; - import { importAccounts } from '../accounts_typed'; -import { normalizeStatus } from './normalizer'; -import { importPolls } from './polls'; +import { normalizeStatus, normalizePoll } from './normalizer'; export const STATUS_IMPORT = 'STATUS_IMPORT'; export const STATUSES_IMPORT = 'STATUSES_IMPORT'; +export const POLLS_IMPORT = 'POLLS_IMPORT'; export const FILTERS_IMPORT = 'FILTERS_IMPORT'; function pushUnique(array, object) { @@ -27,6 +25,10 @@ export function importFilters(filters) { return { type: FILTERS_IMPORT, filters }; } +export function importPolls(polls) { + return { type: POLLS_IMPORT, polls }; +} + export function importFetchedAccount(account) { return importFetchedAccounts([account]); } @@ -75,7 +77,7 @@ export function importFetchedStatuses(statuses) { } if (status.poll?.id) { - pushUnique(polls, createPollFromServerJSON(status.poll, getState().polls[status.poll.id])); + pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); } if (status.card) { @@ -85,9 +87,15 @@ export function importFetchedStatuses(statuses) { statuses.forEach(processStatus); - dispatch(importPolls({ polls })); + dispatch(importPolls(polls)); dispatch(importFetchedAccounts(accounts)); dispatch(importStatuses(normalStatuses)); dispatch(importFilters(filters)); }; } + +export function importFetchedPoll(poll) { + return (dispatch, getState) => { + dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))])); + }; +} diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index b643cf5613..d9e9fef0c6 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,12 +1,15 @@ import escapeTextContentForBrowser from 'escape-html'; -import { makeEmojiMap } from 'mastodon/models/custom_emoji'; - import emojify from '../../features/emoji/emoji'; import { expandSpoilers, me } from '../../initial_state'; const domParser = new DOMParser(); +const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { + obj[`:${emoji.shortcode}:`] = emoji; + return obj; +}, {}); + export function searchTextFromRawStatus (status) { const spoilerText = status.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); @@ -137,6 +140,38 @@ export function normalizeStatusTranslation(translation, status) { return normalTranslation; } +export function normalizePoll(poll, normalOldPoll) { + const normalPoll = { ...poll }; + const emojiMap = makeEmojiMap(poll.emojis); + + normalPoll.options = poll.options.map((option, index) => { + const normalOption = { + ...option, + voted: poll.own_votes && poll.own_votes.includes(index), + titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), + }; + + if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) { + normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']); + } + + return normalOption; + }); + + return normalPoll; +} + +export function normalizePollOptionTranslation(translation, poll) { + const emojiMap = makeEmojiMap(poll.get('emojis').toJS()); + + const normalTranslation = { + ...translation, + titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap), + }; + + return normalTranslation; +} + export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; const emojiMap = makeEmojiMap(normalAnnouncement.emojis); diff --git a/app/javascript/mastodon/actions/importer/polls.ts b/app/javascript/mastodon/actions/importer/polls.ts deleted file mode 100644 index 5bbe7d57d6..0000000000 --- a/app/javascript/mastodon/actions/importer/polls.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import type { Poll } from 'mastodon/models/poll'; - -export const importPolls = createAction<{ polls: Poll[] }>( - 'poll/importMultiple', -); diff --git a/app/javascript/mastodon/actions/modal.ts b/app/javascript/mastodon/actions/modal.ts index 49af176a11..ab03e46765 100644 --- a/app/javascript/mastodon/actions/modal.ts +++ b/app/javascript/mastodon/actions/modal.ts @@ -9,7 +9,6 @@ export type ModalType = keyof typeof MODAL_COMPONENTS; interface OpenModalPayload { modalType: ModalType; modalProps: ModalProps; - previousModalProps?: ModalProps; } export const openModal = createAction('MODAL_OPEN'); diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index c7b192accc..20daf30042 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -12,7 +12,7 @@ import type { } from 'mastodon/api_types/notifications'; import { allNotificationTypes } from 'mastodon/api_types/notifications'; import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; -import { enableEmojiReaction, usePendingItems } from 'mastodon/initial_state'; +import { usePendingItems } from 'mastodon/initial_state'; import type { NotificationGap } from 'mastodon/reducers/notification_groups'; import { selectSettingsNotificationsExcludedTypes, @@ -37,15 +37,9 @@ function excludeAllTypesExcept(filter: string) { function getExcludedTypes(state: RootState) { const activeFilter = selectSettingsNotificationsQuickFilterActive(state); - const types = - activeFilter === 'all' - ? selectSettingsNotificationsExcludedTypes(state) - : excludeAllTypesExcept(activeFilter); - if (!enableEmojiReaction && !types.includes('emoji_reaction')) { - types.push('emoji_reaction'); - } - - return types; + return activeFilter === 'all' + ? selectSettingsNotificationsExcludedTypes(state) + : excludeAllTypesExcept(activeFilter); } function dispatchAssociatedRecords( @@ -161,7 +155,7 @@ export const processNewNotificationForGroups = createAppAsyncThunk( const showInColumn = activeFilter === 'all' - ? notificationShows[notification.type] !== false + ? notificationShows[notification.type] : activeFilter === notification.type; if (!showInColumn) return; diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 87b842e51f..f8b2aa13a4 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,25 +1,57 @@ import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; +import { List as ImmutableList } from 'immutable'; + +import { compareId } from 'mastodon/compare_id'; +import { enableEmojiReaction, usePendingItems as preferPendingItems } from 'mastodon/initial_state'; + +import api, { getLinks } from '../api'; import { unescapeHTML } from '../utils/html'; import { requestNotificationPermission } from '../utils/notifications'; import { fetchFollowRequests } from './accounts'; import { importFetchedAccount, + importFetchedAccounts, + importFetchedStatus, + importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; import { notificationsUpdate } from "./notifications_typed"; import { register as registerPushNotifications } from './push_notifications'; +import { saveSettings } from './settings'; import { STATUS_EMOJI_REACTION_UPDATE } from './statuses'; export * from "./notifications_typed"; +export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; + +export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; +export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; +export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; + export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; + +export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; +export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; + +export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; + export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; +export const NOTIFICATION_REQUESTS_ACCEPT_REQUEST = 'NOTIFICATION_REQUESTS_ACCEPT_REQUEST'; +export const NOTIFICATION_REQUESTS_ACCEPT_SUCCESS = 'NOTIFICATION_REQUESTS_ACCEPT_SUCCESS'; +export const NOTIFICATION_REQUESTS_ACCEPT_FAIL = 'NOTIFICATION_REQUESTS_ACCEPT_FAIL'; + +export const NOTIFICATION_REQUESTS_DISMISS_REQUEST = 'NOTIFICATION_REQUESTS_DISMISS_REQUEST'; +export const NOTIFICATION_REQUESTS_DISMISS_SUCCESS = 'NOTIFICATION_REQUESTS_DISMISS_SUCCESS'; +export const NOTIFICATION_REQUESTS_DISMISS_FAIL = 'NOTIFICATION_REQUESTS_DISMISS_FAIL'; + const messages = defineMessages({ // mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, @@ -37,6 +69,10 @@ const messages = defineMessages({ message_update: { id: 'notification.update', defaultMessage: '{name} edited a post' }, }); +export const loadPending = () => ({ + type: NOTIFICATIONS_LOAD_PENDING, +}); + export function updateEmojiReactions(emoji_reaction) { return (dispatch) => dispatch({ @@ -47,6 +83,8 @@ export function updateEmojiReactions(emoji_reaction) { export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { + const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); + const showInColumn = activeFilter === 'all' ? getState().getIn(['settings', 'notifications', 'shows', notification.type], true) : activeFilter === notification.type; const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); @@ -55,7 +93,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { if (['mention', 'status'].includes(notification.type) && notification.status.filtered) { const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); - if (filters.some(result => result.filter.filter_action === 'hide')) { + if (filters.some(result => result.filter.filter_action_ex === 'hide')) { return; } @@ -68,9 +106,25 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch(submitMarkers()); - // `notificationsUpdate` is still used in `user_lists` and `relationships` reducers - dispatch(importFetchedAccount(notification.account)); - dispatch(notificationsUpdate({ notification, playSound: playSound && !filtered})); + if (showInColumn) { + dispatch(importFetchedAccount(notification.account)); + + if (notification.status) { + dispatch(importFetchedStatus(notification.status)); + } + + if (notification.report) { + dispatch(importFetchedAccount(notification.report.target_account)); + } + + + dispatch(notificationsUpdate({ notification, preferPendingItems, playSound: playSound && !filtered})); + } else if (playSound && !filtered) { + dispatch({ + type: NOTIFICATIONS_UPDATE_NOOP, + meta: { sound: 'boop' }, + }); + } // Desktop notifications if (typeof window.Notification !== 'undefined' && showAlert && !filtered) { @@ -91,8 +145,149 @@ export function updateNotifications(notification, intlMessages, intlLocale) { }; } +const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + +const excludeTypesFromFilter = filter => { + const allTypes = ImmutableList([ + 'follow', + 'follow_request', + 'favourite', + 'emoji_reaction', + 'reblog', + 'status_reference', + 'mention', + 'poll', + 'status', + 'list_status', + 'update', + 'admin.sign_up', + 'admin.report', + ]); + + return allTypes.filterNot(item => item === filter).toJS(); +}; + const noOp = () => {}; +let expandNotificationsController = new AbortController(); + +export function expandNotifications({ maxId = undefined, forceLoad = false }) { + return async (dispatch, getState) => { + const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); + const notifications = getState().get('notifications'); + const isLoadingMore = !!maxId; + + if (notifications.get('isLoading')) { + if (forceLoad) { + expandNotificationsController.abort(); + expandNotificationsController = new AbortController(); + } else { + return; + } + } + + let exclude_types = activeFilter === 'all' + ? excludeTypesFromSettings(getState()) + : excludeTypesFromFilter(activeFilter); + if (!enableEmojiReaction && !exclude_types.includes('emoji_reaction')) { + exclude_types.push('emoji_reaction'); + } + + const params = { + max_id: maxId, + exclude_types, + }; + + if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { + const a = notifications.getIn(['pendingItems', 0, 'id']); + const b = notifications.getIn(['items', 0, 'id']); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } + } + + const isLoadingRecent = !!params.since_id; + + dispatch(expandNotificationsRequest(isLoadingMore)); + + try { + const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }); + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.map(item => item.account))); + dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); + dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account))); + + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); + dispatch(submitMarkers()); + } catch(error) { + dispatch(expandNotificationsFail(error, isLoadingMore)); + } + }; +} + +export function expandNotificationsRequest(isLoadingMore) { + return { + type: NOTIFICATIONS_EXPAND_REQUEST, + skipLoading: !isLoadingMore, + }; +} + +export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { + return { + type: NOTIFICATIONS_EXPAND_SUCCESS, + notifications, + next, + isLoadingRecent: isLoadingRecent, + usePendingItems, + skipLoading: !isLoadingMore, + }; +} + +export function expandNotificationsFail(error, isLoadingMore) { + return { + type: NOTIFICATIONS_EXPAND_FAIL, + error, + skipLoading: !isLoadingMore, + skipAlert: !isLoadingMore || error.name === 'AbortError', + }; +} + +export function scrollTopNotifications(top) { + return { + type: NOTIFICATIONS_SCROLL_TOP, + top, + }; +} + +export function setFilter (filterType) { + return dispatch => { + dispatch({ + type: NOTIFICATIONS_FILTER_SET, + path: ['notifications', 'quickFilter', 'active'], + value: filterType, + }); + dispatch(expandNotifications({ forceLoad: true })); + dispatch(saveSettings()); + }; +} + +export const mountNotifications = () => ({ + type: NOTIFICATIONS_MOUNT, +}); + +export const unmountNotifications = () => ({ + type: NOTIFICATIONS_UNMOUNT, +}); + + +export const markNotificationsAsRead = () => ({ + type: NOTIFICATIONS_MARK_AS_READ, +}); + // Browser support export function setupBrowserNotifications() { return dispatch => { diff --git a/app/javascript/mastodon/actions/notifications_migration.tsx b/app/javascript/mastodon/actions/notifications_migration.tsx new file mode 100644 index 0000000000..cd9f5ca3d6 --- /dev/null +++ b/app/javascript/mastodon/actions/notifications_migration.tsx @@ -0,0 +1,10 @@ +import { createAppAsyncThunk } from 'mastodon/store'; + +import { fetchNotifications } from './notification_groups'; + +export const initializeNotifications = createAppAsyncThunk( + 'notifications/initialize', + (_, { dispatch }) => { + void dispatch(fetchNotifications()); + }, +); diff --git a/app/javascript/mastodon/actions/notifications_typed.ts b/app/javascript/mastodon/actions/notifications_typed.ts index 3eb1230666..88d942d45e 100644 --- a/app/javascript/mastodon/actions/notifications_typed.ts +++ b/app/javascript/mastodon/actions/notifications_typed.ts @@ -9,6 +9,7 @@ export const notificationsUpdate = createAction( ...args }: { notification: ApiNotificationJSON; + usePendingItems: boolean; playSound: boolean; }) => ({ payload: args, diff --git a/app/javascript/mastodon/actions/polls.js b/app/javascript/mastodon/actions/polls.js new file mode 100644 index 0000000000..aa49341444 --- /dev/null +++ b/app/javascript/mastodon/actions/polls.js @@ -0,0 +1,61 @@ +import api from '../api'; + +import { importFetchedPoll } from './importer'; + +export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; +export const POLL_VOTE_SUCCESS = 'POLL_VOTE_SUCCESS'; +export const POLL_VOTE_FAIL = 'POLL_VOTE_FAIL'; + +export const POLL_FETCH_REQUEST = 'POLL_FETCH_REQUEST'; +export const POLL_FETCH_SUCCESS = 'POLL_FETCH_SUCCESS'; +export const POLL_FETCH_FAIL = 'POLL_FETCH_FAIL'; + +export const vote = (pollId, choices) => (dispatch) => { + dispatch(voteRequest()); + + api().post(`/api/v1/polls/${pollId}/votes`, { choices }) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(voteSuccess(data)); + }) + .catch(err => dispatch(voteFail(err))); +}; + +export const fetchPoll = pollId => (dispatch) => { + dispatch(fetchPollRequest()); + + api().get(`/api/v1/polls/${pollId}`) + .then(({ data }) => { + dispatch(importFetchedPoll(data)); + dispatch(fetchPollSuccess(data)); + }) + .catch(err => dispatch(fetchPollFail(err))); +}; + +export const voteRequest = () => ({ + type: POLL_VOTE_REQUEST, +}); + +export const voteSuccess = poll => ({ + type: POLL_VOTE_SUCCESS, + poll, +}); + +export const voteFail = error => ({ + type: POLL_VOTE_FAIL, + error, +}); + +export const fetchPollRequest = () => ({ + type: POLL_FETCH_REQUEST, +}); + +export const fetchPollSuccess = poll => ({ + type: POLL_FETCH_SUCCESS, + poll, +}); + +export const fetchPollFail = error => ({ + type: POLL_FETCH_FAIL, + error, +}); diff --git a/app/javascript/mastodon/actions/polls.ts b/app/javascript/mastodon/actions/polls.ts deleted file mode 100644 index 65a96e8f62..0000000000 --- a/app/javascript/mastodon/actions/polls.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { apiGetPoll, apiPollVote } from 'mastodon/api/polls'; -import type { ApiPollJSON } from 'mastodon/api_types/polls'; -import { createPollFromServerJSON } from 'mastodon/models/poll'; -import { - createAppAsyncThunk, - createDataLoadingThunk, -} from 'mastodon/store/typed_functions'; - -import { importPolls } from './importer/polls'; - -export const importFetchedPoll = createAppAsyncThunk( - 'poll/importFetched', - (args: { poll: ApiPollJSON }, { dispatch, getState }) => { - const { poll } = args; - - dispatch( - importPolls({ - polls: [createPollFromServerJSON(poll, getState().polls[poll.id])], - }), - ); - }, -); - -export const vote = createDataLoadingThunk( - 'poll/vote', - ({ pollId, choices }: { pollId: string; choices: string[] }) => - apiPollVote(pollId, choices), - async (poll, { dispatch, discardLoadData }) => { - await dispatch(importFetchedPoll({ poll })); - return discardLoadData; - }, -); - -export const fetchPoll = createDataLoadingThunk( - 'poll/fetch', - ({ pollId }: { pollId: string }) => apiGetPoll(pollId), - async (poll, { dispatch }) => { - await dispatch(importFetchedPoll({ poll })); - }, -); diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js index 647a6bd9fb..b3d3850e31 100644 --- a/app/javascript/mastodon/actions/push_notifications/registerer.js +++ b/app/javascript/mastodon/actions/push_notifications/registerer.js @@ -33,7 +33,7 @@ const unsubscribe = ({ registration, subscription }) => subscription ? subscription.unsubscribe().then(() => registration) : registration; const sendSubscriptionToBackend = (subscription) => { - const params = { subscription: { ...subscription.toJSON(), standard: true } }; + const params = { subscription }; if (me) { const data = pushNotificationsSetting.get(me); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js new file mode 100644 index 0000000000..bde17ae0db --- /dev/null +++ b/app/javascript/mastodon/actions/search.js @@ -0,0 +1,215 @@ +import { fromJS } from 'immutable'; + +import { searchHistory } from 'mastodon/settings'; + +import api from '../api'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts, importFetchedStatuses } from './importer'; + +export const SEARCH_CHANGE = 'SEARCH_CHANGE'; +export const SEARCH_CLEAR = 'SEARCH_CLEAR'; +export const SEARCH_SHOW = 'SEARCH_SHOW'; + +export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; +export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; +export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; + +export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST'; +export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS'; +export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL'; + +export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE'; + +export function changeSearch(value) { + return { + type: SEARCH_CHANGE, + value, + }; +} + +export function clearSearch() { + return { + type: SEARCH_CLEAR, + }; +} + +export function submitSearch(type) { + return (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + const signedIn = !!getState().getIn(['meta', 'me']); + + if (value.length === 0) { + dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type)); + return; + } + + dispatch(fetchSearchRequest(type)); + + api().get('/api/v2/search', { + params: { + q: value, + resolve: signedIn, + limit: 11, + type, + }, + }).then(response => { + if (response.data.accounts) { + dispatch(importFetchedAccounts(response.data.accounts)); + } + + if (response.data.statuses) { + dispatch(importFetchedStatuses(response.data.statuses)); + } + + dispatch(fetchSearchSuccess(response.data, value, type)); + dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); + }).catch(error => { + dispatch(fetchSearchFail(error)); + }); + }; +} + +export function fetchSearchRequest(searchType) { + return { + type: SEARCH_FETCH_REQUEST, + searchType, + }; +} + +export function fetchSearchSuccess(results, searchTerm, searchType) { + return { + type: SEARCH_FETCH_SUCCESS, + results, + searchType, + searchTerm, + }; +} + +export function fetchSearchFail(error) { + return { + type: SEARCH_FETCH_FAIL, + error, + }; +} + +export const expandSearch = type => (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + const offset = getState().getIn(['search', 'results', type]).size - 1; + + dispatch(expandSearchRequest(type)); + + api().get('/api/v2/search', { + params: { + q: value, + type, + offset, + limit: 11, + }, + }).then(({ data }) => { + if (data.accounts) { + dispatch(importFetchedAccounts(data.accounts)); + } + + if (data.statuses) { + dispatch(importFetchedStatuses(data.statuses)); + } + + dispatch(expandSearchSuccess(data, value, type)); + dispatch(fetchRelationships(data.accounts.map(item => item.id))); + }).catch(error => { + dispatch(expandSearchFail(error)); + }); +}; + +export const expandSearchRequest = (searchType) => ({ + type: SEARCH_EXPAND_REQUEST, + searchType, +}); + +export const expandSearchSuccess = (results, searchTerm, searchType) => ({ + type: SEARCH_EXPAND_SUCCESS, + results, + searchTerm, + searchType, +}); + +export const expandSearchFail = error => ({ + type: SEARCH_EXPAND_FAIL, + error, +}); + +export const showSearch = () => ({ + type: SEARCH_SHOW, +}); + +export const openURL = (value, history, onFailure) => (dispatch, getState) => { + const signedIn = !!getState().getIn(['meta', 'me']); + + if (!signedIn) { + if (onFailure) { + onFailure(); + } + + return; + } + + dispatch(fetchSearchRequest()); + + api().get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => { + if (response.data.accounts?.length > 0) { + dispatch(importFetchedAccounts(response.data.accounts)); + history.push(`/@${response.data.accounts[0].acct}`); + } else if (response.data.statuses?.length > 0) { + dispatch(importFetchedStatuses(response.data.statuses)); + history.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`); + } else if (onFailure) { + onFailure(); + } + + dispatch(fetchSearchSuccess(response.data, value)); + }).catch(err => { + dispatch(fetchSearchFail(err)); + + if (onFailure) { + onFailure(); + } + }); +}; + +export const clickSearchResult = (q, type) => (dispatch, getState) => { + const previous = getState().getIn(['search', 'recent']); + + if (previous.some(x => x.get('q') === q && x.get('type') === type)) { + return; + } + + const me = getState().getIn(['meta', 'me']); + const current = previous.add(fromJS({ type, q })).takeLast(4); + + searchHistory.set(me, current.toJS()); + dispatch(updateSearchHistory(current)); +}; + +export const forgetSearchResult = q => (dispatch, getState) => { + const previous = getState().getIn(['search', 'recent']); + const me = getState().getIn(['meta', 'me']); + const current = previous.filterNot(result => result.get('q') === q); + + searchHistory.set(me, current.toJS()); + dispatch(updateSearchHistory(current)); +}; + +export const updateSearchHistory = recent => ({ + type: SEARCH_HISTORY_UPDATE, + recent, +}); + +export const hydrateSearch = () => (dispatch, getState) => { + const me = getState().getIn(['meta', 'me']); + const history = searchHistory.get(me); + + if (history !== null) { + dispatch(updateSearchHistory(history)); + } +}; diff --git a/app/javascript/mastodon/actions/search.ts b/app/javascript/mastodon/actions/search.ts deleted file mode 100644 index 13a4ee4432..0000000000 --- a/app/javascript/mastodon/actions/search.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; - -import { apiGetSearch } from 'mastodon/api/search'; -import type { ApiSearchType } from 'mastodon/api_types/search'; -import type { - RecentSearch, - SearchType as RecentSearchType, -} from 'mastodon/models/search'; -import { searchHistory } from 'mastodon/settings'; -import { - createDataLoadingThunk, - createAppAsyncThunk, -} from 'mastodon/store/typed_functions'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts, importFetchedStatuses } from './importer'; - -export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE'; - -export const submitSearch = createDataLoadingThunk( - 'search/submit', - async ({ q, type }: { q: string; type?: ApiSearchType }, { getState }) => { - const signedIn = !!getState().meta.get('me'); - - return apiGetSearch({ - q, - type, - resolve: signedIn, - limit: 11, - }); - }, - (data, { dispatch }) => { - if (data.accounts.length > 0) { - dispatch(importFetchedAccounts(data.accounts)); - dispatch(fetchRelationships(data.accounts.map((account) => account.id))); - } - - if (data.statuses.length > 0) { - dispatch(importFetchedStatuses(data.statuses)); - } - - return data; - }, - { - useLoadingBar: false, - }, -); - -export const expandSearch = createDataLoadingThunk( - 'search/expand', - async ({ type }: { type: ApiSearchType }, { getState }) => { - const q = getState().search.q; - const results = getState().search.results; - const offset = results?.[type].length; - - return apiGetSearch({ - q, - type, - limit: 10, - offset, - }); - }, - (data, { dispatch }) => { - if (data.accounts.length > 0) { - dispatch(importFetchedAccounts(data.accounts)); - dispatch(fetchRelationships(data.accounts.map((account) => account.id))); - } - - if (data.statuses.length > 0) { - dispatch(importFetchedStatuses(data.statuses)); - } - - return data; - }, - { - useLoadingBar: true, - }, -); - -export const openURL = createDataLoadingThunk( - 'search/openURL', - ({ url }: { url: string }) => - apiGetSearch({ - q: url, - resolve: true, - limit: 1, - }), - (data, { dispatch }) => { - if (data.accounts.length > 0) { - dispatch(importFetchedAccounts(data.accounts)); - } else if (data.statuses.length > 0) { - dispatch(importFetchedStatuses(data.statuses)); - } - - return data; - }, - { - useLoadingBar: true, - }, -); - -export const clickSearchResult = createAppAsyncThunk( - 'search/clickResult', - ( - { q, type }: { q: string; type?: RecentSearchType }, - { dispatch, getState }, - ) => { - const previous = getState().search.recent; - - if (previous.some((x) => x.q === q && x.type === type)) { - return; - } - - const me = getState().meta.get('me') as string; - const current = [{ type, q }, ...previous].slice(0, 4); - - searchHistory.set(me, current); - dispatch(updateSearchHistory(current)); - }, -); - -export const forgetSearchResult = createAppAsyncThunk( - 'search/forgetResult', - (q: string, { dispatch, getState }) => { - const previous = getState().search.recent; - const me = getState().meta.get('me') as string; - const current = previous.filter((result) => result.q !== q); - - searchHistory.set(me, current); - dispatch(updateSearchHistory(current)); - }, -); - -export const updateSearchHistory = createAction( - 'search/updateHistory', -); - -export const hydrateSearch = createAppAsyncThunk( - 'search/hydrate', - (_args, { dispatch, getState }) => { - const me = getState().meta.get('me') as string; - const history = searchHistory.get(me) as RecentSearch[] | null; - - if (history !== null) { - dispatch(updateSearchHistory(history)); - } - }, -); diff --git a/app/javascript/mastodon/actions/settings.js b/app/javascript/mastodon/actions/settings.js index 7659fb5f98..fbd89f9d4b 100644 --- a/app/javascript/mastodon/actions/settings.js +++ b/app/javascript/mastodon/actions/settings.js @@ -29,7 +29,7 @@ const debouncedSave = debounce((dispatch, getState) => { api().put('/api/web/settings', { data }) .then(() => dispatch({ type: SETTING_SAVE })) .catch(error => dispatch(showAlertForError(error))); -}, 2000, { leading: true, trailing: true }); +}, 5000, { trailing: true }); export function saveSettings() { return (dispatch, getState) => debouncedSave(dispatch, getState); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 5064e65e7b..40ead34782 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -148,7 +148,7 @@ export function deleteStatus(id, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => { + api().delete(`/api/v1/statuses/${id}`).then(response => { dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); dispatch(importFetchedAccount(response.data.account)); diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index e8fec13453..8ab75cdc44 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,4 +1,4 @@ -import { fromJS, isIndexed } from 'immutable'; +import { Iterable, fromJS } from 'immutable'; import { hydrateCompose } from './compose'; import { importFetchedAccounts } from './importer'; @@ -9,7 +9,8 @@ export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => fromJS(rawState, (k, v) => - isIndexed(v) ? v.toList() : v.toMap()); + Iterable.isIndexed(v) ? v.toList() : v.toMap()); + export function hydrateStore(rawState) { return dispatch => { diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index f9d784c2b4..57aed6bc36 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -11,7 +11,7 @@ import { } from './announcements'; import { updateConversations } from './conversations'; import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups'; -import { updateNotifications, updateEmojiReactions } from './notifications'; +import { updateNotifications, expandNotifications, updateEmojiReactions } from './notifications'; import { updateStatus } from './statuses'; import { updateTimeline, @@ -112,6 +112,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti dispatch(updateEmojiReactions(JSON.parse(data.payload))); break; case 'notifications_merged': { + const state = getState(); + if (state.notifications.top || !state.notifications.mounted) + dispatch(expandNotifications({ forceLoad: true, maxId: undefined })); dispatch(refreshStaleNotificationGroups()); break; } diff --git a/app/javascript/mastodon/actions/tags.js b/app/javascript/mastodon/actions/tags.js new file mode 100644 index 0000000000..d18d7e514f --- /dev/null +++ b/app/javascript/mastodon/actions/tags.js @@ -0,0 +1,172 @@ +import api, { getLinks } from '../api'; + +export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST'; +export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS'; +export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL'; + +export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST'; +export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS'; +export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; + +export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST'; +export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; +export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; + +export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST'; +export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS'; +export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL'; + +export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST'; +export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS'; +export const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL'; + +export const fetchHashtag = name => (dispatch) => { + dispatch(fetchHashtagRequest()); + + api().get(`/api/v1/tags/${name}`).then(({ data }) => { + dispatch(fetchHashtagSuccess(name, data)); + }).catch(err => { + dispatch(fetchHashtagFail(err)); + }); +}; + +export const fetchHashtagRequest = () => ({ + type: HASHTAG_FETCH_REQUEST, +}); + +export const fetchHashtagSuccess = (name, tag) => ({ + type: HASHTAG_FETCH_SUCCESS, + name, + tag, +}); + +export const fetchHashtagFail = error => ({ + type: HASHTAG_FETCH_FAIL, + error, +}); + +export const fetchFollowedHashtags = () => (dispatch) => { + dispatch(fetchFollowedHashtagsRequest()); + + api().get('/api/v1/followed_tags').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(fetchFollowedHashtagsFail(err)); + }); +}; + +export function fetchFollowedHashtagsRequest() { + return { + type: FOLLOWED_HASHTAGS_FETCH_REQUEST, + }; +} + +export function fetchFollowedHashtagsSuccess(followed_tags, next) { + return { + type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, + followed_tags, + next, + }; +} + +export function fetchFollowedHashtagsFail(error) { + return { + type: FOLLOWED_HASHTAGS_FETCH_FAIL, + error, + }; +} + +export function expandFollowedHashtags() { + return (dispatch, getState) => { + const url = getState().getIn(['followed_tags', 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowedHashtagsRequest()); + + api().get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandFollowedHashtagsFail(error)); + }); + }; +} + +export function expandFollowedHashtagsRequest() { + return { + type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, + }; +} + +export function expandFollowedHashtagsSuccess(followed_tags, next) { + return { + type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, + followed_tags, + next, + }; +} + +export function expandFollowedHashtagsFail(error) { + return { + type: FOLLOWED_HASHTAGS_EXPAND_FAIL, + error, + }; +} + +export const followHashtag = name => (dispatch) => { + dispatch(followHashtagRequest(name)); + + api().post(`/api/v1/tags/${name}/follow`).then(({ data }) => { + dispatch(followHashtagSuccess(name, data)); + }).catch(err => { + dispatch(followHashtagFail(name, err)); + }); +}; + +export const followHashtagRequest = name => ({ + type: HASHTAG_FOLLOW_REQUEST, + name, +}); + +export const followHashtagSuccess = (name, tag) => ({ + type: HASHTAG_FOLLOW_SUCCESS, + name, + tag, +}); + +export const followHashtagFail = (name, error) => ({ + type: HASHTAG_FOLLOW_FAIL, + name, + error, +}); + +export const unfollowHashtag = name => (dispatch) => { + dispatch(unfollowHashtagRequest(name)); + + api().post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => { + dispatch(unfollowHashtagSuccess(name, data)); + }).catch(err => { + dispatch(unfollowHashtagFail(name, err)); + }); +}; + +export const unfollowHashtagRequest = name => ({ + type: HASHTAG_UNFOLLOW_REQUEST, + name, +}); + +export const unfollowHashtagSuccess = (name, tag) => ({ + type: HASHTAG_UNFOLLOW_SUCCESS, + name, + tag, +}); + +export const unfollowHashtagFail = (name, error) => ({ + type: HASHTAG_UNFOLLOW_FAIL, + name, + error, +}); diff --git a/app/javascript/mastodon/actions/tags_typed.ts b/app/javascript/mastodon/actions/tags_typed.ts deleted file mode 100644 index 6dca32fd84..0000000000 --- a/app/javascript/mastodon/actions/tags_typed.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; - -export const fetchHashtag = createDataLoadingThunk( - 'tags/fetch', - ({ tagId }: { tagId: string }) => apiGetTag(tagId), -); - -export const followHashtag = createDataLoadingThunk( - 'tags/follow', - ({ tagId }: { tagId: string }) => apiFollowTag(tagId), -); - -export const unfollowHashtag = createDataLoadingThunk( - 'tags/unfollow', - ({ tagId }: { tagId: string }) => apiUnfollowTag(tagId), -); diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index a41b058d2c..f0663ded40 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -1,9 +1,4 @@ -import type { - AxiosError, - AxiosResponse, - Method, - RawAxiosRequestHeaders, -} from 'axios'; +import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; import axios from 'axios'; import LinkHeader from 'http-link-header'; @@ -46,7 +41,7 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => { // eslint-disable-next-line import/no-default-export export default function api(withAuthorization = true) { - const instance = axios.create({ + return axios.create({ transitional: { clarifyTimeoutError: true, }, @@ -65,22 +60,6 @@ export default function api(withAuthorization = true) { }, ], }); - - instance.interceptors.response.use( - (response: AxiosResponse) => { - if (response.headers.deprecation) { - console.warn( - `Deprecated request: ${response.config.method} ${response.config.url}`, - ); - } - return response; - }, - (error: AxiosError) => { - return Promise.reject(error); - }, - ); - - return instance; } type RequestParamsOrData = Record; diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 717010ba74..bd1757e827 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -5,16 +5,3 @@ export const apiSubmitAccountNote = (id: string, value: string) => apiRequestPost(`v1/accounts/${id}/note`, { comment: value, }); - -export const apiFollowAccount = ( - id: string, - params?: { - reblogs: boolean; - }, -) => - apiRequestPost(`v1/accounts/${id}/follow`, { - ...params, - }); - -export const apiUnfollowAccount = (id: string) => - apiRequestPost(`v1/accounts/${id}/unfollow`); diff --git a/app/javascript/mastodon/api/compose.ts b/app/javascript/mastodon/api/compose.ts deleted file mode 100644 index 757e9961c9..0000000000 --- a/app/javascript/mastodon/api/compose.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { apiRequestPut } from 'mastodon/api'; -import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; - -export const apiUpdateMedia = ( - id: string, - params?: { description?: string; focus?: string }, -) => apiRequestPut(`v1/media/${id}`, params); diff --git a/app/javascript/mastodon/api/domain_blocks.ts b/app/javascript/mastodon/api/domain_blocks.ts deleted file mode 100644 index 4e153b0ee9..0000000000 --- a/app/javascript/mastodon/api/domain_blocks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import api, { getLinks } from 'mastodon/api'; - -export const apiGetDomainBlocks = async (url?: string) => { - const response = await api().request({ - method: 'GET', - url: url ?? '/api/v1/domain_blocks', - }); - - return { - domains: response.data, - links: getLinks(response), - }; -}; diff --git a/app/javascript/mastodon/api/instance.ts b/app/javascript/mastodon/api/instance.ts deleted file mode 100644 index 764e8daab2..0000000000 --- a/app/javascript/mastodon/api/instance.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { apiRequestGet } from 'mastodon/api'; -import type { - ApiTermsOfServiceJSON, - ApiPrivacyPolicyJSON, -} from 'mastodon/api_types/instance'; - -export const apiGetTermsOfService = (version?: string) => - apiRequestGet( - version - ? `v1/instance/terms_of_service/${version}` - : 'v1/instance/terms_of_service', - ); - -export const apiGetPrivacyPolicy = () => - apiRequestGet('v1/instance/privacy_policy'); diff --git a/app/javascript/mastodon/api/polls.ts b/app/javascript/mastodon/api/polls.ts deleted file mode 100644 index cb659986f5..0000000000 --- a/app/javascript/mastodon/api/polls.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { apiRequestGet, apiRequestPost } from 'mastodon/api'; -import type { ApiPollJSON } from 'mastodon/api_types/polls'; - -export const apiGetPoll = (pollId: string) => - apiRequestGet(`/v1/polls/${pollId}`); - -export const apiPollVote = (pollId: string, choices: string[]) => - apiRequestPost(`/v1/polls/${pollId}/votes`, { - choices, - }); diff --git a/app/javascript/mastodon/api/search.ts b/app/javascript/mastodon/api/search.ts deleted file mode 100644 index 79b0385fe8..0000000000 --- a/app/javascript/mastodon/api/search.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { apiRequestGet } from 'mastodon/api'; -import type { - ApiSearchType, - ApiSearchResultsJSON, -} from 'mastodon/api_types/search'; - -export const apiGetSearch = (params: { - q: string; - resolve?: boolean; - type?: ApiSearchType; - limit?: number; - offset?: number; -}) => - apiRequestGet('v2/search', { - ...params, - }); diff --git a/app/javascript/mastodon/api/tags.ts b/app/javascript/mastodon/api/tags.ts deleted file mode 100644 index 4b111def81..0000000000 --- a/app/javascript/mastodon/api/tags.ts +++ /dev/null @@ -1,23 +0,0 @@ -import api, { getLinks, apiRequestPost, apiRequestGet } from 'mastodon/api'; -import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; - -export const apiGetTag = (tagId: string) => - apiRequestGet(`v1/tags/${tagId}`); - -export const apiFollowTag = (tagId: string) => - apiRequestPost(`v1/tags/${tagId}/follow`); - -export const apiUnfollowTag = (tagId: string) => - apiRequestPost(`v1/tags/${tagId}/unfollow`); - -export const apiGetFollowedTags = async (url?: string) => { - const response = await api().request({ - method: 'GET', - url: url ?? '/api/v1/followed_tags', - }); - - return { - tags: response.data, - links: getLinks(response), - }; -}; diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index 9d7974eda0..80d575cad6 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -45,7 +45,7 @@ export interface BaseApiAccountJSON { avatar_static: string; bot: boolean; created_at: string; - discoverable?: boolean; + discoverable: boolean; indexable: boolean; display_name: string; emojis: ApiCustomEmojiJSON[]; diff --git a/app/javascript/mastodon/api_types/antennas.ts b/app/javascript/mastodon/api_types/antennas.ts index a2a8a997ba..75a8835276 100644 --- a/app/javascript/mastodon/api_types/antennas.ts +++ b/app/javascript/mastodon/api_types/antennas.ts @@ -10,7 +10,6 @@ export interface ApiAntennaJSON { insert_feeds: boolean; with_media_only: boolean; ignore_reblog: boolean; - favourite: boolean; list: ApiListJSON | null; list_id: string | undefined; diff --git a/app/javascript/mastodon/api_types/instance.ts b/app/javascript/mastodon/api_types/instance.ts deleted file mode 100644 index 3a29684b70..0000000000 --- a/app/javascript/mastodon/api_types/instance.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface ApiTermsOfServiceJSON { - effective_date: string; - effective: boolean; - succeeded_by: string | null; - content: string; -} - -export interface ApiPrivacyPolicyJSON { - updated_at: string; - content: string; -} diff --git a/app/javascript/mastodon/api_types/lists.ts b/app/javascript/mastodon/api_types/lists.ts index bc32b33883..d7a8247bbe 100644 --- a/app/javascript/mastodon/api_types/lists.ts +++ b/app/javascript/mastodon/api_types/lists.ts @@ -10,6 +10,5 @@ export interface ApiListJSON { exclusive: boolean; replies_policy: RepliesPolicyType; notify: boolean; - favourite: boolean; antennas?: ApiAntennaJSON[]; } diff --git a/app/javascript/mastodon/api_types/polls.ts b/app/javascript/mastodon/api_types/polls.ts index 891a2faba7..8181f7b813 100644 --- a/app/javascript/mastodon/api_types/polls.ts +++ b/app/javascript/mastodon/api_types/polls.ts @@ -13,11 +13,11 @@ export interface ApiPollJSON { expired: boolean; multiple: boolean; votes_count: number; - voters_count: number | null; + voters_count: number; options: ApiPollOptionJSON[]; emojis: ApiCustomEmojiJSON[]; - voted?: boolean; - own_votes?: number[]; + voted: boolean; + own_votes: number[]; } diff --git a/app/javascript/mastodon/api_types/search.ts b/app/javascript/mastodon/api_types/search.ts deleted file mode 100644 index 795cbb2b41..0000000000 --- a/app/javascript/mastodon/api_types/search.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ApiAccountJSON } from './accounts'; -import type { ApiStatusJSON } from './statuses'; -import type { ApiHashtagJSON } from './tags'; - -export type ApiSearchType = 'accounts' | 'statuses' | 'hashtags'; - -export interface ApiSearchResultsJSON { - accounts: ApiAccountJSON[]; - statuses: ApiStatusJSON[]; - hashtags: ApiHashtagJSON[]; -} diff --git a/app/javascript/mastodon/api_types/tags.ts b/app/javascript/mastodon/api_types/tags.ts deleted file mode 100644 index 0c16c8bd28..0000000000 --- a/app/javascript/mastodon/api_types/tags.ts +++ /dev/null @@ -1,13 +0,0 @@ -interface ApiHistoryJSON { - day: string; - accounts: string; - uses: string; -} - -export interface ApiHashtagJSON { - id: string; - name: string; - url: string; - history: [ApiHistoryJSON, ...ApiHistoryJSON[]]; - following?: boolean; -} diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx new file mode 100644 index 0000000000..3c46dfc10b --- /dev/null +++ b/app/javascript/mastodon/components/account.jsx @@ -0,0 +1,188 @@ +import PropTypes from 'prop-types'; +import { useCallback } from 'react'; + +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; +import { Link } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; +import { EmptyAccount } from 'mastodon/components/empty_account'; +import { FollowButton } from 'mastodon/components/follow_button'; +import { ShortNumber } from 'mastodon/components/short_number'; +import { VerifiedBadge } from 'mastodon/components/verified_badge'; + +import DropdownMenuContainer from '../containers/dropdown_menu_container'; +import { me } from '../initial_state'; + +import { Avatar } from './avatar'; +import { Button } from './button'; +import { FollowersCounter } from './counters'; +import { DisplayName } from './display_name'; +import { RelativeTimestamp } from './relative_timestamp'; + +const messages = defineMessages({ + unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, + mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' }, + unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' }, + mute: { id: 'account.mute_short', defaultMessage: 'Mute' }, + block: { id: 'account.block_short', defaultMessage: 'Block' }, + more: { id: 'status.more', defaultMessage: 'More' }, +}); + +const Account = ({ size = 46, account, onBlock, onMute, onMuteNotifications, hidden, hideButtons, minimal, defaultAction, children, withBio }) => { + const intl = useIntl(); + + const handleBlock = useCallback(() => { + onBlock(account); + }, [onBlock, account]); + + const handleMute = useCallback(() => { + onMute(account); + }, [onMute, account]); + + const handleMuteNotifications = useCallback(() => { + onMuteNotifications(account, true); + }, [onMuteNotifications, account]); + + const handleUnmuteNotifications = useCallback(() => { + onMuteNotifications(account, false); + }, [onMuteNotifications, account]); + + if (!account) { + return ; + } + + if (hidden) { + return ( + <> + {account.get('display_name')} + {account.get('username')} + + ); + } + + let buttons; + + if (!hideButtons && account.get('id') !== me && account.get('relationship', null) !== null) { + const requested = account.getIn(['relationship', 'requested']); + const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); + + if (requested) { + buttons = ; + } else if (blocking) { + buttons = - )} - - - ); -}; - -export const AlertsController: React.FC = () => { - const alerts = useAppSelector((state) => state.alerts); - - if (alerts.length === 0) { - return null; - } - - return ( -

- {alerts.map((alert, idx) => ( - - ))} -
- ); -}; diff --git a/app/javascript/mastodon/components/alt_text_badge.tsx b/app/javascript/mastodon/components/alt_text_badge.tsx index 701cfbe8b4..99bec1ee51 100644 --- a/app/javascript/mastodon/components/alt_text_badge.tsx +++ b/app/javascript/mastodon/components/alt_text_badge.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef, useId } from 'react'; +import { useState, useCallback, useRef } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -8,15 +8,12 @@ import type { UsePopperOptions, } from 'react-overlays/esm/usePopper'; -import { useSelectableClick } from 'mastodon/hooks/useSelectableClick'; - const offset = [0, 4] as OffsetValue; const popperConfig = { strategy: 'fixed' } as UsePopperOptions; export const AltTextBadge: React.FC<{ description: string; }> = ({ description }) => { - const accessibilityId = useId(); const anchorRef = useRef(null); const [open, setOpen] = useState(false); @@ -28,16 +25,12 @@ export const AltTextBadge: React.FC<{ setOpen(false); }, [setOpen]); - const [handleMouseDown, handleMouseUp] = useSelectableClick(handleClose); - return ( <> @@ -54,12 +47,9 @@ export const AltTextBadge: React.FC<{ > {({ props }) => (
-

= ({ value }) => { const [previousValue, setPreviousValue] = useState(value); - const direction = value > previousValue ? -1 : 1; + const [direction, setDirection] = useState<1 | -1>(1); - const [styles, api] = useSpring( - () => ({ - from: { transform: `translateY(${100 * direction}%)` }, - to: { transform: 'translateY(0%)' }, - onRest() { - setPreviousValue(value); - }, - config: { ...config.gentle, duration: 200 }, - immediate: true, // This ensures that the animation is not played when the component is first rendered - }), - [value, previousValue], + 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], ); - // When the value changes, start the animation - useEffect(() => { - if (value !== previousValue) { - void api.start({ reset: true }); - } - }, [api, previousValue, value]); - if (reduceMotion) { return ; } + const styles = [ + { + key: `${value}`, + data: value, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }, + ]; + return ( - - - - - {value !== previousValue && ( - - - + + {(items) => ( + + {items.map(({ key, data, style }) => ( + 0 ? 'absolute' : 'static', + transform: `translateY(${(style.y ?? 0) * 100}%)`, + }} + > + + + ))} + )} - + ); }; diff --git a/app/javascript/mastodon/components/attachment_list.jsx b/app/javascript/mastodon/components/attachment_list.jsx index f97e22f2d4..c5ac046751 100644 --- a/app/javascript/mastodon/components/attachment_list.jsx +++ b/app/javascript/mastodon/components/attachment_list.jsx @@ -36,7 +36,7 @@ export default class AttachmentList extends ImmutablePureComponent { return (
  • - + {compact && } {compact && ' ' } {displayUrl ? filename(displayUrl) : } diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx index a2dc0b782e..f61d9676de 100644 --- a/app/javascript/mastodon/components/avatar.tsx +++ b/app/javascript/mastodon/components/avatar.tsx @@ -2,7 +2,7 @@ import { useState, useCallback } from 'react'; import classNames from 'classnames'; -import { useHovering } from 'mastodon/hooks/useHovering'; +import { useHovering } from 'mastodon/../hooks/useHovering'; import { autoPlayGif } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx index 0bd33fea69..f98cfcc38b 100644 --- a/app/javascript/mastodon/components/avatar_overlay.tsx +++ b/app/javascript/mastodon/components/avatar_overlay.tsx @@ -1,7 +1,8 @@ -import { useHovering } from 'mastodon/hooks/useHovering'; -import { autoPlayGif } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; +import { useHovering } from '../../hooks/useHovering'; +import { autoPlayGif } from '../initial_state'; + interface Props { account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there diff --git a/app/javascript/mastodon/components/button.tsx b/app/javascript/mastodon/components/button.tsx index a527468f65..b349a83f2b 100644 --- a/app/javascript/mastodon/components/button.tsx +++ b/app/javascript/mastodon/components/button.tsx @@ -7,7 +7,6 @@ interface BaseProps extends Omit, 'children'> { block?: boolean; secondary?: boolean; - compact?: boolean; dangerous?: boolean; } @@ -28,7 +27,6 @@ export const Button: React.FC = ({ disabled, block, secondary, - compact, dangerous, className, title, @@ -49,7 +47,6 @@ export const Button: React.FC = ({ +
  • ); diff --git a/app/javascript/mastodon/components/dropdown_menu.jsx b/app/javascript/mastodon/components/dropdown_menu.jsx new file mode 100644 index 0000000000..4d1a47a2f5 --- /dev/null +++ b/app/javascript/mastodon/components/dropdown_menu.jsx @@ -0,0 +1,345 @@ +import PropTypes from 'prop-types'; +import { PureComponent, cloneElement, Children } from 'react'; + +import classNames from 'classnames'; +import { withRouter } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import { supportsPassiveEvents } from 'detect-passive-events'; +import Overlay from 'react-overlays/Overlay'; + +import { CircularProgress } from 'mastodon/components/circular_progress'; +import { WithRouterPropTypes } from 'mastodon/utils/react_router'; + +import { IconButton } from './icon_button'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; +let id = 0; + +class DropdownMenu extends PureComponent { + + static propTypes = { + items: PropTypes.array.isRequired, + loading: PropTypes.bool, + scrollable: PropTypes.bool, + onClose: PropTypes.func.isRequired, + style: PropTypes.object, + openedViaKeyboard: PropTypes.bool, + renderItem: PropTypes.func, + renderHeader: PropTypes.func, + onItemClick: PropTypes.func.isRequired, + }; + + static defaultProps = { + style: {}, + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + e.stopPropagation(); + e.preventDefault(); + } + }; + + componentDidMount () { + 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) { + this.focusedItem.focus({ preventScroll: true }); + } + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + }; + + setFocusRef = c => { + this.focusedItem = c; + }; + + handleKeyDown = e => { + const items = Array.from(this.node.querySelectorAll('a, button')); + const index = items.indexOf(document.activeElement); + let element = null; + + switch(e.key) { + case 'ArrowDown': + element = items[index+1] || items[0]; + break; + case 'ArrowUp': + element = items[index-1] || items[items.length-1]; + break; + case 'Tab': + if (e.shiftKey) { + element = items[index-1] || items[items.length-1]; + } else { + element = items[index+1] || items[0]; + } + break; + case 'Home': + element = items[0]; + break; + case 'End': + element = items[items.length-1]; + break; + case 'Escape': + this.props.onClose(); + break; + } + + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + }; + + handleItemKeyPress = e => { + if (e.key === 'Enter' || e.key === ' ') { + this.handleClick(e); + } + }; + + handleClick = e => { + const { onItemClick } = this.props; + onItemClick(e); + }; + + renderItem = (option, i) => { + if (option === null) { + return
  • ; + } + + const { text, href = '#', target = '_blank', method, dangerous } = option; + + return ( +
  • + + {text} + +
  • + ); + }; + + render () { + const { items, scrollable, renderHeader, loading } = this.props; + + let renderItem = this.props.renderItem || this.renderItem; + + return ( +
    + {loading && ( + + )} + + {!loading && renderHeader && ( +
    + {renderHeader(items)} +
    + )} + + {!loading && ( +
      + {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))} +
    + )} +
    + ); + } + +} + +class Dropdown extends PureComponent { + + static propTypes = { + children: PropTypes.node, + icon: PropTypes.string, + iconComponent: PropTypes.func, + items: PropTypes.array.isRequired, + loading: PropTypes.bool, + size: PropTypes.number, + title: PropTypes.string, + disabled: PropTypes.bool, + scrollable: PropTypes.bool, + active: PropTypes.bool, + status: ImmutablePropTypes.map, + isUserTouching: PropTypes.func, + onOpen: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + openDropdownId: PropTypes.number, + openedViaKeyboard: PropTypes.bool, + renderItem: PropTypes.func, + renderHeader: PropTypes.func, + onItemClick: PropTypes.func, + ...WithRouterPropTypes + }; + + static defaultProps = { + title: 'Menu', + }; + + state = { + id: id++, + }; + + handleClick = ({ type }) => { + if (this.state.id === this.props.openDropdownId) { + this.handleClose(); + } else { + this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click'); + } + }; + + handleClose = () => { + if (this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + this.activeElement = null; + } + this.props.onClose(this.state.id); + }; + + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + }; + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + }; + + handleKeyPress = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleClick(e); + e.stopPropagation(); + e.preventDefault(); + break; + } + }; + + handleItemClick = e => { + const { onItemClick } = this.props; + const i = Number(e.currentTarget.getAttribute('data-index')); + const item = this.props.items[i]; + + this.handleClose(); + + if (typeof onItemClick === 'function') { + e.preventDefault(); + onItemClick(item, i); + } else if (item && typeof item.action === 'function') { + e.preventDefault(); + item.action(); + } else if (item && item.to) { + e.preventDefault(); + this.props.history.push(item.to); + } + }; + + setTargetRef = c => { + this.target = c; + }; + + findTarget = () => { + return this.target?.buttonRef?.current ?? this.target; + }; + + componentWillUnmount = () => { + if (this.state.id === this.props.openDropdownId) { + this.handleClose(); + } + }; + + close = () => { + this.handleClose(); + }; + + render () { + const { + icon, + iconComponent, + items, + size, + title, + disabled, + loading, + scrollable, + openDropdownId, + openedViaKeyboard, + children, + renderItem, + renderHeader, + active, + } = this.props; + + const open = this.state.id === openDropdownId; + + const button = children ? cloneElement(Children.only(children), { + onClick: this.handleClick, + onMouseDown: this.handleMouseDown, + onKeyDown: this.handleButtonKeyDown, + onKeyPress: this.handleKeyPress, + ref: this.setTargetRef, + }) : ( + + ); + + return ( + <> + {button} + + + {({ props, arrowProps, placement }) => ( +
    +
    +
    + +
    +
    + )} + + + ); + } + +} + +export default withRouter(Dropdown); diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx deleted file mode 100644 index 0f9ab5b1cc..0000000000 --- a/app/javascript/mastodon/components/dropdown_menu.tsx +++ /dev/null @@ -1,551 +0,0 @@ -import { - useState, - useEffect, - useRef, - useCallback, - cloneElement, - Children, -} from 'react'; - -import classNames from 'classnames'; -import { Link } from 'react-router-dom'; - -import type { Map as ImmutableMap } from 'immutable'; - -import Overlay from 'react-overlays/Overlay'; -import type { - OffsetValue, - UsePopperOptions, -} from 'react-overlays/esm/usePopper'; - -import { fetchRelationships } from 'mastodon/actions/accounts'; -import { - openDropdownMenu, - closeDropdownMenu, -} from 'mastodon/actions/dropdown_menu'; -import { openModal, closeModal } from 'mastodon/actions/modal'; -import { CircularProgress } from 'mastodon/components/circular_progress'; -import { isUserTouching } from 'mastodon/is_mobile'; -import type { - MenuItem, - ActionMenuItem, - ExternalLinkMenuItem, -} from 'mastodon/models/dropdown_menu'; -import { useAppDispatch, useAppSelector } from 'mastodon/store'; - -import type { IconProp } from './icon'; -import { IconButton } from './icon_button'; - -let id = 0; - -const isMenuItem = (item: unknown): item is MenuItem => { - if (item === null) { - return true; - } - - return typeof item === 'object' && 'text' in item; -}; - -const isActionItem = (item: unknown): item is ActionMenuItem => { - if (!item || !isMenuItem(item)) { - return false; - } - - return 'action' in item; -}; - -const isExternalLinkItem = (item: unknown): item is ExternalLinkMenuItem => { - if (!item || !isMenuItem(item)) { - return false; - } - - return 'href' in item; -}; - -type RenderItemFn = ( - item: Item, - index: number, - handlers: { - onClick: (e: React.MouseEvent) => void; - onKeyUp: (e: React.KeyboardEvent) => void; - }, -) => React.ReactNode; - -type ItemClickFn = (item: Item, index: number) => void; - -type RenderHeaderFn = (items: Item[]) => React.ReactNode; - -interface DropdownMenuProps { - items?: Item[]; - loading?: boolean; - scrollable?: boolean; - onClose: () => void; - openedViaKeyboard: boolean; - renderItem?: RenderItemFn; - renderHeader?: RenderHeaderFn; - onItemClick?: ItemClickFn; -} - -export const DropdownMenu = ({ - items, - loading, - scrollable, - onClose, - openedViaKeyboard, - renderItem, - renderHeader, - onItemClick, -}: DropdownMenuProps) => { - const nodeRef = useRef(null); - const focusedItemRef = useRef(null); - - useEffect(() => { - const handleDocumentClick = (e: MouseEvent) => { - if ( - e.target instanceof Node && - nodeRef.current && - !nodeRef.current.contains(e.target) - ) { - onClose(); - e.stopPropagation(); - e.preventDefault(); - } - }; - - const handleKeyDown = (e: KeyboardEvent) => { - if (!nodeRef.current) { - return; - } - - const items = Array.from(nodeRef.current.querySelectorAll('a, button')); - const index = document.activeElement - ? items.indexOf(document.activeElement) - : -1; - - let element: Element | undefined; - - switch (e.key) { - case 'ArrowDown': - element = items[index + 1] ?? items[0]; - break; - case 'ArrowUp': - element = items[index - 1] ?? items[items.length - 1]; - break; - case 'Tab': - if (e.shiftKey) { - element = items[index - 1] ?? items[items.length - 1]; - } else { - element = items[index + 1] ?? items[0]; - } - break; - case 'Home': - element = items[0]; - break; - case 'End': - element = items[items.length - 1]; - break; - case 'Escape': - onClose(); - break; - } - - if (element && element instanceof HTMLElement) { - element.focus(); - e.preventDefault(); - e.stopPropagation(); - } - }; - - document.addEventListener('click', handleDocumentClick, { capture: true }); - document.addEventListener('keydown', handleKeyDown, { capture: true }); - - if (focusedItemRef.current && openedViaKeyboard) { - focusedItemRef.current.focus({ preventScroll: true }); - } - - return () => { - document.removeEventListener('click', handleDocumentClick, { - capture: true, - }); - document.removeEventListener('keydown', handleKeyDown, { capture: true }); - }; - }, [onClose, openedViaKeyboard]); - - const handleFocusedItemRef = useCallback( - (c: HTMLAnchorElement | HTMLButtonElement | null) => { - focusedItemRef.current = c as HTMLElement; - }, - [], - ); - - const handleItemClick = useCallback( - (e: React.MouseEvent | React.KeyboardEvent) => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const item = items?.[i]; - - onClose(); - - if (!item) { - return; - } - - if (typeof onItemClick === 'function') { - e.preventDefault(); - onItemClick(item, i); - } else if (isActionItem(item)) { - e.preventDefault(); - item.action(); - } - }, - [onClose, onItemClick, items], - ); - - const handleItemKeyUp = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - handleItemClick(e); - } - }, - [handleItemClick], - ); - - const nativeRenderItem = (option: Item, i: number) => { - if (!isMenuItem(option)) { - return null; - } - - if (option === null) { - return
  • ; - } - - const { text, dangerous } = option; - - let element: React.ReactElement; - - if (isActionItem(option)) { - element = ( - - ); - } else if (isExternalLinkItem(option)) { - element = ( - - {text} - - ); - } else { - element = ( - - {text} - - ); - } - - return ( -
  • - {element} -
  • - ); - }; - - const renderItemMethod = renderItem ?? nativeRenderItem; - - return ( -
    - {(loading || !items) && } - - {!loading && renderHeader && items && ( -
    - {renderHeader(items)} -
    - )} - - {!loading && items && ( -
      - {items.map((option, i) => - renderItemMethod(option, i, { - onClick: handleItemClick, - onKeyUp: handleItemKeyUp, - }), - )} -
    - )} -
    - ); -}; - -interface DropdownProps { - children?: React.ReactElement; - icon?: string; - iconComponent?: IconProp; - items?: Item[]; - loading?: boolean; - title?: string; - disabled?: boolean; - scrollable?: boolean; - active?: boolean; - scrollKey?: string; - status?: ImmutableMap; - renderItem?: RenderItemFn; - renderHeader?: RenderHeaderFn; - onOpen?: () => void; - onItemClick?: ItemClickFn; -} - -const offset = [5, 5] as OffsetValue; -const popperConfig = { strategy: 'fixed' } as UsePopperOptions; - -export const Dropdown = ({ - children, - icon, - iconComponent, - items, - loading, - title = 'Menu', - disabled, - scrollable, - active, - status, - renderItem, - renderHeader, - onOpen, - onItemClick, - scrollKey, -}: DropdownProps) => { - const dispatch = useAppDispatch(); - const openDropdownId = useAppSelector((state) => state.dropdownMenu.openId); - const openedViaKeyboard = useAppSelector( - (state) => state.dropdownMenu.keyboard, - ); - const [currentId] = useState(id++); - const open = currentId === openDropdownId; - const activeElement = useRef(null); - const targetRef = useRef(null); - - const handleClose = useCallback(() => { - if (activeElement.current) { - activeElement.current.focus({ preventScroll: true }); - activeElement.current = null; - } - - dispatch( - closeModal({ - modalType: 'ACTIONS', - ignoreFocus: false, - }), - ); - - dispatch(closeDropdownMenu({ id: currentId })); - }, [dispatch, currentId]); - - const handleItemClick = useCallback( - (e: React.MouseEvent | React.KeyboardEvent) => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const item = items?.[i]; - - handleClose(); - - if (!item) { - return; - } - - if (typeof onItemClick === 'function') { - e.preventDefault(); - onItemClick(item, i); - } else if (isActionItem(item)) { - e.preventDefault(); - item.action(); - } - }, - [handleClose, onItemClick, items], - ); - - const handleClick = useCallback( - (e: React.MouseEvent | React.KeyboardEvent) => { - const { type } = e; - - if (open) { - handleClose(); - } else { - onOpen?.(); - - if (status) { - dispatch(fetchRelationships([status.getIn(['account', 'id'])])); - } - - if (isUserTouching()) { - dispatch( - openModal({ - modalType: 'ACTIONS', - modalProps: { - status, - actions: items, - onClick: handleItemClick, - }, - }), - ); - } else { - dispatch( - openDropdownMenu({ - id: currentId, - keyboard: type !== 'click', - scrollKey, - }), - ); - } - } - }, - [ - dispatch, - currentId, - scrollKey, - onOpen, - handleItemClick, - open, - status, - items, - handleClose, - ], - ); - - const handleMouseDown = useCallback(() => { - if (!open && document.activeElement instanceof HTMLElement) { - activeElement.current = document.activeElement; - } - }, [open]); - - const handleButtonKeyDown = useCallback( - (e: React.KeyboardEvent) => { - switch (e.key) { - case ' ': - case 'Enter': - handleMouseDown(); - break; - } - }, - [handleMouseDown], - ); - - const handleKeyPress = useCallback( - (e: React.KeyboardEvent) => { - switch (e.key) { - case ' ': - case 'Enter': - handleClick(e); - e.stopPropagation(); - e.preventDefault(); - break; - } - }, - [handleClick], - ); - - useEffect(() => { - return () => { - if (currentId === openDropdownId) { - handleClose(); - } - }; - }, [currentId, openDropdownId, handleClose]); - - let button: React.ReactElement; - - if (children) { - button = cloneElement(Children.only(children), { - onClick: handleClick, - onMouseDown: handleMouseDown, - onKeyDown: handleButtonKeyDown, - onKeyPress: handleKeyPress, - ref: targetRef, - }); - } else if (icon && iconComponent) { - button = ( - - ); - } else { - return null; - } - - return ( - <> - {button} - - - {({ props, arrowProps, placement }) => ( -
    -
    -
    - - -
    -
    - )} - - - ); -}; diff --git a/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js new file mode 100644 index 0000000000..726fee9076 --- /dev/null +++ b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js @@ -0,0 +1,32 @@ +import { connect } from 'react-redux'; + +import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_menu'; +import { fetchHistory } from 'mastodon/actions/history'; +import DropdownMenu from 'mastodon/components/dropdown_menu'; + +/** + * + * @param {import('mastodon/store').RootState} state + * @param {*} props + */ +const mapStateToProps = (state, { statusId }) => ({ + openDropdownId: state.dropdownMenu.openId, + openedViaKeyboard: state.dropdownMenu.keyboard, + items: state.getIn(['history', statusId, 'items']), + loading: state.getIn(['history', statusId, 'loading']), +}); + +const mapDispatchToProps = (dispatch, { statusId }) => ({ + + onOpen (id, onItemClick, keyboard) { + dispatch(fetchHistory(statusId)); + dispatch(openDropdownMenu({ id, keyboard })); + }, + + onClose (id) { + dispatch(closeDropdownMenu({ id })); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); diff --git a/app/javascript/mastodon/components/edited_timestamp/index.jsx b/app/javascript/mastodon/components/edited_timestamp/index.jsx new file mode 100644 index 0000000000..fbf14ec4bd --- /dev/null +++ b/app/javascript/mastodon/components/edited_timestamp/index.jsx @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedMessage, injectIntl } from 'react-intl'; + +import { connect } from 'react-redux'; + +import { openModal } from 'mastodon/actions/modal'; +import InlineAccount from 'mastodon/components/inline_account'; +import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; + +import DropdownMenu from './containers/dropdown_menu_container'; + +const mapDispatchToProps = (dispatch, { statusId }) => ({ + + onItemClick (index) { + dispatch(openModal({ + modalType: 'COMPARE_HISTORY', + modalProps: { index, statusId }, + })); + }, + +}); + +class EditedTimestamp extends PureComponent { + + static propTypes = { + statusId: PropTypes.string.isRequired, + timestamp: PropTypes.string.isRequired, + intl: PropTypes.object.isRequired, + onItemClick: PropTypes.func.isRequired, + }; + + handleItemClick = (item, i) => { + const { onItemClick } = this.props; + onItemClick(i); + }; + + renderHeader = items => { + return ( + + ); + }; + + renderItem = (item, index, { onClick, onKeyPress }) => { + const formattedDate = ; + const formattedName = ; + + const label = item.get('original') ? ( + + ) : ( + + ); + + return ( +
  • + +
  • + ); + }; + + render () { + const { timestamp, intl, statusId } = this.props; + + return ( + + + + ); + } + +} + +export default connect(null, mapDispatchToProps)(injectIntl(EditedTimestamp)); diff --git a/app/javascript/mastodon/components/edited_timestamp/index.tsx b/app/javascript/mastodon/components/edited_timestamp/index.tsx deleted file mode 100644 index 4a33210199..0000000000 --- a/app/javascript/mastodon/components/edited_timestamp/index.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { useCallback } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -import { fetchHistory } from 'mastodon/actions/history'; -import { openModal } from 'mastodon/actions/modal'; -import { Dropdown } from 'mastodon/components/dropdown_menu'; -import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; -import InlineAccount from 'mastodon/components/inline_account'; -import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; -import { useAppDispatch, useAppSelector } from 'mastodon/store'; - -type HistoryItem = ImmutableMap; - -export const EditedTimestamp: React.FC<{ - statusId: string; - timestamp: string; -}> = ({ statusId, timestamp }) => { - const dispatch = useAppDispatch(); - const items = useAppSelector( - (state) => - ( - state.history.getIn([statusId, 'items']) as - | ImmutableList - | undefined - )?.toArray() as HistoryItem[], - ); - const loading = useAppSelector( - (state) => state.history.getIn([statusId, 'loading']) as boolean, - ); - - const handleOpen = useCallback(() => { - dispatch(fetchHistory(statusId)); - }, [dispatch, statusId]); - - const handleItemClick = useCallback( - (_item: HistoryItem, index: number) => { - dispatch( - openModal({ - modalType: 'COMPARE_HISTORY', - modalProps: { index, statusId }, - }), - ); - }, - [dispatch, statusId], - ); - - const renderHeader = useCallback((items: HistoryItem[]) => { - return ( - - ); - }, []); - - const renderItem = useCallback( - ( - item: HistoryItem, - index: number, - { - onClick, - onKeyUp, - }: { - onClick: React.MouseEventHandler; - onKeyUp: React.KeyboardEventHandler; - }, - ) => { - const formattedDate = ( - - ); - const formattedName = ( - - ); - - const label = (item.get('original') as boolean) ? ( - - ) : ( - - ); - - return ( -
  • - -
  • - ); - }, - [], - ); - - return ( - - items={items} - loading={loading} - renderItem={renderItem} - scrollable - renderHeader={renderHeader} - onOpen={handleOpen} - onItemClick={handleItemClick} - > - - - ); -}; diff --git a/app/javascript/mastodon/components/empty_account.tsx b/app/javascript/mastodon/components/empty_account.tsx new file mode 100644 index 0000000000..a4a6b7f823 --- /dev/null +++ b/app/javascript/mastodon/components/empty_account.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import { DisplayName } from 'mastodon/components/display_name'; +import { Skeleton } from 'mastodon/components/skeleton'; + +interface Props { + size?: number; + minimal?: boolean; +} + +export const EmptyAccount: React.FC = ({ + size = 46, + minimal = false, +}) => { + return ( +
    +
    +
    +
    + +
    + +
    + + +
    +
    +
    +
    + ); +}; diff --git a/app/javascript/mastodon/components/error_boundary.jsx b/app/javascript/mastodon/components/error_boundary.jsx index ca2f017f3b..392a3ad61e 100644 --- a/app/javascript/mastodon/components/error_boundary.jsx +++ b/app/javascript/mastodon/components/error_boundary.jsx @@ -98,7 +98,7 @@ export default class ErrorBoundary extends PureComponent { )}

    -

    Mastodon v{version} · ·

    +

    Mastodon v{version} · ·

    diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index f21ad60240..9bb89bf2b5 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -2,8 +2,6 @@ import { useCallback, useEffect } from 'react'; import { useIntl, defineMessages } from 'react-intl'; -import classNames from 'classnames'; - import { useIdentity } from '@/mastodon/identity_context'; import { fetchRelationships, followAccount } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; @@ -16,13 +14,13 @@ const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' }, - editProfile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, + mutual: { id: 'account.mutual', defaultMessage: 'Mutual' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, }); export const FollowButton: React.FC<{ accountId?: string; - compact?: boolean; -}> = ({ accountId, compact }) => { +}> = ({ accountId }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const { signedIn } = useIdentity(); @@ -54,7 +52,7 @@ export const FollowButton: React.FC<{ ); } - if (!relationship || !accountId) return; + if (!relationship) return; if (accountId === me) { return; @@ -72,9 +70,15 @@ export const FollowButton: React.FC<{ if (!signedIn) { label = intl.formatMessage(messages.follow); } else if (accountId === me) { - label = intl.formatMessage(messages.editProfile); + label = intl.formatMessage(messages.edit_profile); } else if (!relationship) { label = ; + } else if ( + relationship.following && + isShowItem('relationships') && + relationship.followed_by + ) { + label = intl.formatMessage(messages.mutual); } else if (relationship.following || relationship.requested) { label = intl.formatMessage(messages.unfollow); } else if (relationship.followed_by && isShowItem('relationships')) { @@ -88,10 +92,8 @@ export const FollowButton: React.FC<{ {label} @@ -108,7 +110,6 @@ export const FollowButton: React.FC<{ (account?.suspended || !!account?.moved)) } secondary={following} - compact={compact} className={following ? 'button--destructive' : undefined} > {label} diff --git a/app/javascript/mastodon/components/formatted_date.tsx b/app/javascript/mastodon/components/formatted_date.tsx deleted file mode 100644 index cc927b0873..0000000000 --- a/app/javascript/mastodon/components/formatted_date.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import type { ComponentProps } from 'react'; - -import { FormattedDate } from 'react-intl'; - -export const FormattedDateWrapper = ( - props: ComponentProps & { className?: string }, -) => ( - - {(date) => ( - - )} - -); - -const tryIsoString = (date?: string | number | Date): string => { - if (!date) { - return ''; - } - try { - return new Date(date).toISOString(); - } catch { - return date.toString(); - } -}; diff --git a/app/javascript/mastodon/components/gif.tsx b/app/javascript/mastodon/components/gif.tsx deleted file mode 100644 index 1cc0881a5a..0000000000 --- a/app/javascript/mastodon/components/gif.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useHovering } from 'mastodon/hooks/useHovering'; -import { autoPlayGif } from 'mastodon/initial_state'; - -export const GIF: React.FC<{ - src: string; - staticSrc: string; - className: string; - animate?: boolean; -}> = ({ src, staticSrc, className, animate = autoPlayGif }) => { - const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate); - - return ( - - ); -}; diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx index 8e3a434c14..c2be591128 100644 --- a/app/javascript/mastodon/components/gifv.tsx +++ b/app/javascript/mastodon/components/gifv.tsx @@ -1,70 +1,70 @@ -import { useCallback, useState, forwardRef } from 'react'; +import { useCallback, useState } from 'react'; interface Props { src: string; + key: string; alt?: string; lang?: string; - width?: number; - height?: number; - onClick?: React.MouseEventHandler; - onMouseDown?: React.MouseEventHandler; - onTouchStart?: React.TouchEventHandler; + width: number; + height: number; + onClick?: () => void; } -export const GIFV = forwardRef( - ( - { src, alt, lang, width, height, onClick, onMouseDown, onTouchStart }, - ref, - ) => { - const [loading, setLoading] = useState(true); +export const GIFV: React.FC = ({ + src, + alt, + lang, + width, + height, + onClick, +}) => { + const [loading, setLoading] = useState(true); - const handleLoadedData = useCallback(() => { + const handleLoadedData: React.ReactEventHandler = + useCallback(() => { setLoading(false); }, [setLoading]); - const handleClick = useCallback( - (e: React.MouseEvent) => { + const handleClick: React.MouseEventHandler = useCallback( + (e) => { + if (onClick) { e.stopPropagation(); - onClick?.(e); - }, - [onClick], - ); + onClick(); + } + }, + [onClick], + ); - return ( -
    - {loading && ( - - )} - -
    - ); - }, -); + )} -GIFV.displayName = 'GIFV'; +
    + ); +}; diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx index 346c95183f..8963e4a40d 100644 --- a/app/javascript/mastodon/components/hashtag.tsx +++ b/app/javascript/mastodon/components/hashtag.tsx @@ -12,7 +12,6 @@ import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { ShortNumber } from 'mastodon/components/short_number'; import { Skeleton } from 'mastodon/components/skeleton'; -import type { Hashtag as HashtagType } from 'mastodon/models/tags'; interface SilentErrorBoundaryProps { children: React.ReactNode; @@ -81,32 +80,15 @@ export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => ( /> ); -export const CompatibilityHashtag: React.FC<{ - hashtag: HashtagType; -}> = ({ hashtag }) => ( - (day.uses as unknown as number) * 1) - .reverse()} - /> -); - export interface HashtagProps { className?: string; description?: React.ReactNode; history?: number[]; name: string; - people?: number; + people: number; to: string; uses?: number; withGraph?: boolean; - children?: React.ReactNode; } export const Hashtag: React.FC = ({ @@ -118,7 +100,6 @@ export const Hashtag: React.FC = ({ className, description, withGraph = true, - children, }) => (
    @@ -153,14 +134,12 @@ export const Hashtag: React.FC = ({ 0)} + data={history ? history : Array.from(Array(7)).map(() => 0)} >
    )} - - {children &&
    {children}
    }
    ); diff --git a/app/javascript/mastodon/components/hashtag_bar.tsx b/app/javascript/mastodon/components/hashtag_bar.tsx index ce8f17ddb9..9e1d74bb74 100644 --- a/app/javascript/mastodon/components/hashtag_bar.tsx +++ b/app/javascript/mastodon/components/hashtag_bar.tsx @@ -20,7 +20,6 @@ export type StatusLike = Record<{ contentHTML: string; media_attachments: List; spoiler_text?: string; - account: Record<{ id: string }>; }>; function normalizeHashtag(hashtag: string) { @@ -196,36 +195,19 @@ export function getHashtagBarForStatus(status: StatusLike) { return { statusContentProps, - hashtagBar: ( - - ), + hashtagBar: , }; } -export function getFeaturedHashtagBar( - accountId: string, - acct: string, - tags: string[], -) { - return ( - - ); +export function getFeaturedHashtagBar(acct: string, tags: string[]) { + return ; } const HashtagBar: React.FC<{ hashtags: string[]; - accountId: string; acct?: string; defaultExpanded?: boolean; -}> = ({ hashtags, accountId, acct, defaultExpanded }) => { +}> = ({ hashtags, acct, defaultExpanded }) => { const [expanded, setExpanded] = useState(false); const handleClick = useCallback(() => { setExpanded(true); @@ -246,7 +228,6 @@ const HashtagBar: React.FC<{ #{hashtag} diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx index 38c3306f30..057ef1aaed 100644 --- a/app/javascript/mastodon/components/hover_card_controller.tsx +++ b/app/javascript/mastodon/components/hover_card_controller.tsx @@ -8,8 +8,8 @@ import type { UsePopperOptions, } from 'react-overlays/esm/usePopper'; +import { useTimeout } from 'mastodon/../hooks/useTimeout'; import { HoverCardAccount } from 'mastodon/components/hover_card_account'; -import { useTimeout } from 'mastodon/hooks/useTimeout'; const offset = [-12, 4] as OffsetValue; const enterDelay = 750; diff --git a/app/javascript/mastodon/components/icon_button.tsx b/app/javascript/mastodon/components/icon_button.tsx index 7e0b3e7a22..b7cac35960 100644 --- a/app/javascript/mastodon/components/icon_button.tsx +++ b/app/javascript/mastodon/components/icon_button.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, forwardRef } from 'react'; +import { PureComponent, createRef } from 'react'; import classNames from 'classnames'; @@ -15,110 +15,101 @@ interface Props { onMouseDown?: React.MouseEventHandler; onKeyDown?: React.KeyboardEventHandler; onKeyPress?: React.KeyboardEventHandler; - active?: boolean; + active: boolean; expanded?: boolean; style?: React.CSSProperties; activeStyle?: React.CSSProperties; - disabled?: boolean; + disabled: boolean; inverted?: boolean; - animate?: boolean; - overlay?: boolean; - tabIndex?: number; + animate: boolean; + overlay: boolean; + tabIndex: number; counter?: number; href?: string; - ariaHidden?: boolean; + ariaHidden: boolean; data_id?: string; } +interface States { + activate: boolean; + deactivate: boolean; +} +export class IconButton extends PureComponent { + buttonRef = createRef(); -export const IconButton = forwardRef( - ( - { + static defaultProps = { + active: false, + disabled: false, + animate: false, + overlay: false, + tabIndex: 0, + ariaHidden: false, + }; + + state = { + activate: false, + deactivate: false, + }; + + UNSAFE_componentWillReceiveProps(nextProps: Props) { + if (!nextProps.animate) return; + + if (this.props.active && !nextProps.active) { + this.setState({ activate: false, deactivate: true }); + } else if (!this.props.active && nextProps.active) { + this.setState({ activate: true, deactivate: false }); + } + } + + handleClick: React.MouseEventHandler = (e) => { + e.preventDefault(); + + if (!this.props.disabled && this.props.onClick != null) { + this.props.onClick(e); + } + }; + + handleKeyPress: React.KeyboardEventHandler = (e) => { + if (this.props.onKeyPress && !this.props.disabled) { + this.props.onKeyPress(e); + } + }; + + handleMouseDown: React.MouseEventHandler = (e) => { + if (!this.props.disabled && this.props.onMouseDown) { + this.props.onMouseDown(e); + } + }; + + handleKeyDown: React.KeyboardEventHandler = (e) => { + if (!this.props.disabled && this.props.onKeyDown) { + this.props.onKeyDown(e); + } + }; + + render() { + const style = { + ...this.props.style, + ...(this.props.active ? this.props.activeStyle : {}), + }; + + const { + active, className, + disabled, expanded, icon, iconComponent, inverted, + overlay, + tabIndex, title, counter, href, - style, - activeStyle, - onClick, - onKeyDown, - onKeyPress, - onMouseDown, - active = false, - disabled = false, - animate = false, - overlay = false, - tabIndex = 0, - ariaHidden = false, - data_id = undefined, - }, - buttonRef, - ) => { - const [activate, setActivate] = useState(false); - const [deactivate, setDeactivate] = useState(false); + ariaHidden, + data_id, + } = this.props; - useEffect(() => { - if (!animate) { - return; - } - - if (activate && !active) { - setActivate(false); - setDeactivate(true); - } else if (!activate && active) { - setActivate(true); - setDeactivate(false); - } - }, [setActivate, setDeactivate, animate, active, activate]); - - const handleClick: React.MouseEventHandler = useCallback( - (e) => { - e.preventDefault(); - - if (!disabled) { - onClick?.(e); - } - }, - [disabled, onClick], - ); - - const handleKeyPress: React.KeyboardEventHandler = - useCallback( - (e) => { - if (!disabled) { - onKeyPress?.(e); - } - }, - [disabled, onKeyPress], - ); - - const handleMouseDown: React.MouseEventHandler = - useCallback( - (e) => { - if (!disabled) { - onMouseDown?.(e); - } - }, - [disabled, onMouseDown], - ); - - const handleKeyDown: React.KeyboardEventHandler = - useCallback( - (e) => { - if (!disabled) { - onKeyDown?.(e); - } - }, - [disabled, onKeyDown], - ); - - const buttonStyle = { - ...style, - ...(active ? activeStyle : {}), - }; + const { activate, deactivate } = this.state; const classes = classNames(className, 'icon-button', { active, @@ -157,20 +148,18 @@ export const IconButton = forwardRef( aria-hidden={ariaHidden} title={title} className={classes} - onClick={handleClick} - onMouseDown={handleMouseDown} - onKeyDown={handleKeyDown} - onKeyPress={handleKeyPress} // eslint-disable-line @typescript-eslint/no-deprecated - style={buttonStyle} + onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleKeyDown} + onKeyPress={this.handleKeyPress} + style={style} tabIndex={tabIndex} disabled={disabled} data-id={data_id} - ref={buttonRef} + ref={this.buttonRef} > {contents} ); - }, -); - -IconButton.displayName = 'IconButton'; + } +} diff --git a/app/javascript/mastodon/components/load_gap.tsx b/app/javascript/mastodon/components/load_gap.tsx index 6cbdee6ce5..544b5e1461 100644 --- a/app/javascript/mastodon/components/load_gap.tsx +++ b/app/javascript/mastodon/components/load_gap.tsx @@ -1,10 +1,9 @@ -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { useIntl, defineMessages } from 'react-intl'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import { Icon } from 'mastodon/components/icon'; -import { LoadingIndicator } from 'mastodon/components/loading_indicator'; const messages = defineMessages({ load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, @@ -18,12 +17,10 @@ interface Props { export const LoadGap = ({ disabled, param, onClick }: Props) => { const intl = useIntl(); - const [loading, setLoading] = useState(false); const handleClick = useCallback(() => { - setLoading(true); onClick(param); - }, [setLoading, param, onClick]); + }, [param, onClick]); return ( ); }; diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index 12cf381e5e..f81bfbc1da 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -12,7 +12,6 @@ import { debounce } from 'lodash'; import { AltTextBadge } from 'mastodon/components/alt_text_badge'; import { Blurhash } from 'mastodon/components/blurhash'; -import { SpoilerButton } from 'mastodon/components/spoiler_button'; import { formatTime } from 'mastodon/features/video'; import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state'; @@ -39,7 +38,6 @@ class Item extends PureComponent { state = { loaded: false, - error: false, }; handleMouseEnter = (e) => { @@ -83,10 +81,6 @@ class Item extends PureComponent { this.setState({ loaded: true }); }; - handleImageError = () => { - this.setState({ error: true }); - }; - render () { const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props; @@ -128,7 +122,7 @@ class Item extends PureComponent { if (attachment.get('type') === 'unknown') { return (
    - + {description} ); @@ -189,6 +183,7 @@ class Item extends PureComponent {