Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Tiago Epifânio 2024-08-27 09:48:33 +01:00
commit 922041cb9c
1227 changed files with 34192 additions and 15628 deletions

View File

@ -39,7 +39,7 @@
}, },
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"postCreateCommand": "bin/setup", "postCreateCommand": "COREPACK_ENABLE_DOWNLOAD_PROMPT=0 bin/setup",
"waitFor": "postCreateCommand", "waitFor": "postCreateCommand",
"customizations": { "customizations": {

View File

@ -1,5 +1,5 @@
# This is a sample configuration file. You can generate your configuration # This is a sample configuration file. You can generate your configuration
# with the `rake mastodon:setup` interactive setup wizard, but to customize # with the `bundle exec rails mastodon:setup` interactive setup wizard, but to customize
# your setup even further, you'll need to edit it manually. This sample does # your setup even further, you'll need to edit it manually. This sample does
# not demonstrate all available configuration options. Please look at # not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation. # https://docs.joinmastodon.org/admin/config/ for the full documentation.
@ -68,7 +68,7 @@ DB_PORT=5432
# Secrets # Secrets
# ------- # -------
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web bundle exec rake secret` if you use docker compose) # Generate each with the `RAILS_ENV=production bundle exec rails secret` task (`docker-compose run --rm web bundle exec rails secret` if you use docker compose)
# ------- # -------
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET= OTP_SECRET=
@ -76,7 +76,7 @@ OTP_SECRET=
# Web Push # Web Push
# -------- # --------
# Generate with `rake mastodon:webpush:generate_vapid_key` (first is the private key, second is the public one) # Generate with `bundle exec rails mastodon:webpush:generate_vapid_key` (first is the private key, second is the public one)
# You should only generate this once per instance. If you later decide to change it, all push subscription will # You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe. # be invalidated, requiring the users to access the website again to resubscribe.
# -------- # --------

View File

@ -20,10 +20,6 @@ module.exports = defineConfig({
es6: true, es6: true,
}, },
globals: {
ATTACHMENT_HOST: false,
},
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: [ plugins: [
@ -79,7 +75,7 @@ module.exports = defineConfig({
], ],
}, },
], ],
'no-empty': 'off', 'no-empty': ['error', { "allowEmptyCatch": true }],
'no-restricted-properties': [ 'no-restricted-properties': [
'error', 'error',
{ property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substring', message: 'Use .slice instead of .substring.' },
@ -94,7 +90,6 @@ module.exports = defineConfig({
message: "Use '·' (middle dot) instead of '•' (bullet)", message: "Use '·' (middle dot) instead of '•' (bullet)",
}, },
], ],
'no-self-assign': 'off',
'no-unused-expressions': 'error', 'no-unused-expressions': 'error',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
@ -119,12 +114,10 @@ module.exports = defineConfig({
'react/jsx-tag-spacing': 'error', 'react/jsx-tag-spacing': 'error',
'react/jsx-uses-react': 'off', // not needed with new JSX transform 'react/jsx-uses-react': 'off', // not needed with new JSX transform
'react/jsx-wrap-multilines': 'error', 'react/jsx-wrap-multilines': 'error',
'react/no-deprecated': 'off',
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
'react/self-closing-comp': 'error', '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 // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46
'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/click-events-have-key-events': 'off', 'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/label-has-associated-control': 'off',
'jsx-a11y/media-has-caption': 'off', 'jsx-a11y/media-has-caption': 'off',
@ -139,23 +132,6 @@ module.exports = defineConfig({
// ], // ],
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off', 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off',
// recommended rule is: // recommended rule is:
// 'jsx-a11y/no-noninteractive-element-interactions': [
// 'error',
// {
// body: ['onError', 'onLoad'],
// iframe: ['onError', 'onLoad'],
// img: ['onError', 'onLoad'],
// },
// ],
'jsx-a11y/no-noninteractive-element-interactions': [
'warn',
{
handlers: [
'onClick',
],
},
],
// recommended rule is:
// 'jsx-a11y/no-noninteractive-tabindex': [ // 'jsx-a11y/no-noninteractive-tabindex': [
// 'error', // 'error',
// { // {
@ -165,7 +141,6 @@ module.exports = defineConfig({
// }, // },
// ], // ],
'jsx-a11y/no-noninteractive-tabindex': 'off', 'jsx-a11y/no-noninteractive-tabindex': 'off',
'jsx-a11y/no-onchange': 'off',
// recommended is full 'error' // recommended is full 'error'
'jsx-a11y/no-static-element-interactions': [ 'jsx-a11y/no-static-element-interactions': [
'warn', 'warn',

View File

@ -87,6 +87,7 @@
}, },
{ {
// Update devDependencies every week, with one grouped PR // Update devDependencies every week, with one grouped PR
matchManagers: ['npm'],
matchDepTypes: 'devDependencies', matchDepTypes: 'devDependencies',
matchUpdateTypes: ['patch', 'minor'], matchUpdateTypes: ['patch', 'minor'],
groupName: 'devDependencies (non-major)', groupName: 'devDependencies (non-major)',
@ -95,8 +96,7 @@
{ {
// Group all eslint-related packages with `eslint` in the same PR // Group all eslint-related packages with `eslint` in the same PR
matchManagers: ['npm'], matchManagers: ['npm'],
matchPackageNames: ['eslint'], matchPackageNames: ['eslint', 'eslint-*', '@typescript-eslint/*'],
matchPackagePrefixes: ['eslint-', '@typescript-eslint/'],
matchUpdateTypes: ['patch', 'minor'], matchUpdateTypes: ['patch', 'minor'],
groupName: 'eslint (non-major)', groupName: 'eslint (non-major)',
}, },
@ -112,7 +112,8 @@
}, },
{ {
// Update @types/* packages every week, with one grouped PR // Update @types/* packages every week, with one grouped PR
matchPackagePrefixes: '@types/', matchManagers: ['npm'],
matchPackageNames: '@types/*',
matchUpdateTypes: ['patch', 'minor'], matchUpdateTypes: ['patch', 'minor'],
groupName: 'DefinitelyTyped types (non-major)', groupName: 'DefinitelyTyped types (non-major)',
extends: ['schedule:weekly'], extends: ['schedule:weekly'],
@ -129,23 +130,21 @@
{ {
// Group all RuboCop packages with `rubocop` in the same PR // Group all RuboCop packages with `rubocop` in the same PR
matchManagers: ['bundler'], matchManagers: ['bundler'],
matchPackageNames: ['rubocop'], matchPackageNames: ['rubocop', 'rubocop-*'],
matchPackagePrefixes: ['rubocop-'],
matchUpdateTypes: ['patch', 'minor'], matchUpdateTypes: ['patch', 'minor'],
groupName: 'RuboCop (non-major)', groupName: 'RuboCop (non-major)',
}, },
{ {
// Group all RSpec packages with `rspec` in the same PR // Group all RSpec packages with `rspec` in the same PR
matchManagers: ['bundler'], matchManagers: ['bundler'],
matchPackageNames: ['rspec'], matchPackageNames: ['rspec', 'rspec-*'],
matchPackagePrefixes: ['rspec-'],
matchUpdateTypes: ['patch', 'minor'], matchUpdateTypes: ['patch', 'minor'],
groupName: 'RSpec (non-major)', groupName: 'RSpec (non-major)',
}, },
{ {
// Group all opentelemetry-ruby packages in the same PR // Group all opentelemetry-ruby packages in the same PR
matchManagers: ['bundler'], matchManagers: ['bundler'],
matchPackagePrefixes: ['opentelemetry-'], matchPackageNames: ['opentelemetry-*'],
matchUpdateTypes: ['patch', 'minor'], matchUpdateTypes: ['patch', 'minor'],
groupName: 'opentelemetry-ruby (non-major)', groupName: 'opentelemetry-ruby (non-major)',
}, },

View File

@ -85,7 +85,7 @@ jobs:
tags: ${{ inputs.tags }} tags: ${{ inputs.tags }}
labels: ${{ inputs.labels }} labels: ${{ inputs.labels }}
- uses: docker/build-push-action@v5 - uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ${{ inputs.file_to_build }} file: ${{ inputs.file_to_build }}

View File

@ -35,7 +35,7 @@ jobs:
labels: | labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: | flavor: |
latest=true latest=auto
tags: | tags: |
type=raw,value=edge type=raw,value=edge
type=raw,value=nightly type=raw,value=nightly
@ -56,7 +56,7 @@ jobs:
labels: | labels: |
org.opencontainers.image.description=Nightly build image used for testing purposes org.opencontainers.image.description=Nightly build image used for testing purposes
flavor: | flavor: |
latest=true latest=auto
tags: | tags: |
type=raw,value=edge type=raw,value=edge
type=raw,value=nightly type=raw,value=nightly

View File

@ -1,8 +1,10 @@
name: Bundler Audit name: Bundler Audit
on: on:
merge_group:
push: push:
branches-ignore: branches:
- 'dependabot/**' - 'main'
- 'stable-*'
paths: paths:
- 'Gemfile*' - 'Gemfile*'
- '.ruby-version' - '.ruby-version'

View File

@ -2,9 +2,13 @@ name: Check i18n
on: on:
push: push:
branches: [main] branches:
- 'main'
- 'stable-*'
pull_request: pull_request:
branches: [main] branches:
- 'main'
- 'stable-*'
env: env:
RAILS_ENV: test RAILS_ENV: test

View File

@ -1,11 +1,15 @@
name: 'CodeQL' name: 'CodeQL'
on: on:
merge_group:
push: push:
branches: ['main'] branches:
- 'main'
- 'stable-*'
pull_request: pull_request:
# The branches below must be a subset of the branches above branches:
branches: ['main'] - 'main'
- 'stable-*'
schedule: schedule:
- cron: '22 6 * * 1' - cron: '22 6 * * 1'

View File

@ -26,7 +26,7 @@ jobs:
# Download the translation files from Crowdin # Download the translation files from Crowdin
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@v1 uses: crowdin/github-action@v2
with: with:
config: crowdin-glitch.yml config: crowdin-glitch.yml
upload_sources: false upload_sources: false

View File

@ -1,9 +1,11 @@
name: Crowdin / Upload translations name: Crowdin / Upload translations
on: on:
merge_group:
push: push:
branches: branches:
- main - 'main'
- 'stable-*'
paths: paths:
- crowdin-glitch.yml - crowdin-glitch.yml
- app/javascript/flavours/glitch/locales/en.json - app/javascript/flavours/glitch/locales/en.json
@ -17,13 +19,14 @@ on:
jobs: jobs:
upload-translations: upload-translations:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'mastodon/mastodon'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@v1 uses: crowdin/github-action@v2
with: with:
config: crowdin-glitch.yml config: crowdin-glitch.yml
upload_sources: true upload_sources: true

View File

@ -1,6 +1,10 @@
name: Check formatting name: Check formatting
on: on:
merge_group:
push: push:
branches:
- 'main'
- 'stable-*'
pull_request: pull_request:
jobs: jobs:

View File

@ -1,9 +1,10 @@
name: CSS Linting name: CSS Linting
on: on:
merge_group:
push: push:
branches-ignore: branches:
- 'dependabot/**' - 'main'
- 'renovate/**' - 'stable-*'
paths: paths:
- 'package.json' - 'package.json'
- 'yarn.lock' - 'yarn.lock'

View File

@ -1,9 +1,10 @@
name: Haml Linting name: Haml Linting
on: on:
merge_group:
push: push:
branches-ignore: branches:
- 'dependabot/**' - 'main'
- 'renovate/**' - 'stable-*'
paths: paths:
- '.github/workflows/haml-lint-problem-matcher.json' - '.github/workflows/haml-lint-problem-matcher.json'
- '.github/workflows/lint-haml.yml' - '.github/workflows/lint-haml.yml'

View File

@ -1,9 +1,10 @@
name: JavaScript Linting name: JavaScript Linting
on: on:
merge_group:
push: push:
branches-ignore: branches:
- 'dependabot/**' - 'main'
- 'renovate/**' - 'stable-*'
paths: paths:
- 'package.json' - 'package.json'
- 'yarn.lock' - 'yarn.lock'

View File

@ -1,9 +1,10 @@
name: Ruby Linting name: Ruby Linting
on: on:
merge_group:
push: push:
branches-ignore: branches:
- 'dependabot/**' - 'main'
- 'renovate/**' - 'stable-*'
paths: paths:
- 'Gemfile*' - 'Gemfile*'
- '.rubocop*.yml' - '.rubocop*.yml'

View File

@ -10,6 +10,7 @@ permissions:
jobs: jobs:
label-rebase-needed: label-rebase-needed:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'glitch-soc/mastodon'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}

View File

@ -1,9 +1,10 @@
name: JavaScript Testing name: JavaScript Testing
on: on:
merge_group:
push: push:
branches-ignore: branches:
- 'dependabot/**' - 'main'
- 'renovate/**' - 'stable-*'
paths: paths:
- 'package.json' - 'package.json'
- 'yarn.lock' - 'yarn.lock'

View File

@ -1,29 +1,29 @@
name: Historical data migration test name: Historical data migration test
on: on:
merge_group:
push: push:
branches-ignore: branches:
- 'dependabot/**' - 'main'
- 'renovate/**' - 'stable-*'
paths:
- 'Gemfile*'
- '.ruby-version'
- '**/*.rb'
- '.github/workflows/test-migrations.yml'
- 'lib/tasks/tests.rake'
pull_request: pull_request:
paths:
- 'Gemfile*'
- '.ruby-version'
- '**/*.rb'
- '.github/workflows/test-migrations.yml'
- 'lib/tasks/tests.rake'
jobs: jobs:
pre_job:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations.yml", "lib/tasks/tests.rake"]'
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: pre_job
if: needs.pre_job.outputs.should_skip != 'true'
strategy: strategy:
fail-fast: false fail-fast: false

View File

@ -1,10 +1,11 @@
name: Ruby Testing name: Ruby Testing
on: on:
merge_group:
push: push:
branches-ignore: branches:
- 'dependabot/**' - 'main'
- 'renovate/**' - 'stable-*'
pull_request: pull_request:
env: env:
@ -41,11 +42,24 @@ jobs:
with: with:
onlyProduction: 'true' onlyProduction: 'true'
- name: Cache assets from compilation
uses: actions/cache@v4
with:
path: |
public/assets
public/packs
public/packs-test
tmp/cache/webpacker
key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
restore-keys: |
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}
${{ matrix.mode }}-assets-main
${{ matrix.mode }}-assets
- name: Precompile assets - name: Precompile assets
# Previously had set this, but it's not supported
# export NODE_OPTIONS=--openssl-legacy-provider
run: |- run: |-
./bin/rails assets:precompile bin/rails assets:precompile
- name: Archive asset artifacts - name: Archive asset artifacts
run: | run: |
@ -132,15 +146,17 @@ jobs:
additional-system-dependencies: ffmpeg libpam-dev additional-system-dependencies: ffmpeg libpam-dev
- name: Load database schema - name: Load database schema
run: './bin/rails db:create db:schema:load db:seed' run: |
bin/rails db:setup
bin/flatware fan bin/rails db:test:prepare
- run: bin/rspec - run: bin/flatware rspec -r ./spec/flatware_helper.rb
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
if: matrix.ruby-version == '.ruby-version' if: matrix.ruby-version == '.ruby-version'
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
with: with:
files: coverage/lcov/mastodon.lcov files: coverage/lcov/*.lcov
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@ -221,7 +237,7 @@ jobs:
- name: Load database schema - name: Load database schema
run: './bin/rails db:create db:schema:load db:seed' run: './bin/rails db:create db:schema:load db:seed'
- run: bin/rspec --tag paperclip_processing - run: bin/rspec --tag attachment_processing
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
if: matrix.ruby-version == '.ruby-version' if: matrix.ruby-version == '.ruby-version'
@ -285,9 +301,13 @@ jobs:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
path: './public' path: './'
name: ${{ github.sha }} name: ${{ github.sha }}
- name: Expand archived asset artifacts
run: |
tar xvzf artifacts.tar.gz
- name: Set up Ruby environment - name: Set up Ruby environment
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
@ -405,7 +425,7 @@ jobs:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
path: './public' path: './'
name: ${{ github.sha }} name: ${{ github.sha }}
- name: Set up Ruby environment - name: Set up Ruby environment

2
.nvmrc
View File

@ -1 +1 @@
20.14 20.17

View File

@ -1,21 +1,14 @@
--- ---
AllCops: AllCops:
CacheRootDirectory: tmp CacheRootDirectory: tmp
DisplayCopNames: true
DisplayStyleGuide: true DisplayStyleGuide: true
Exclude: Exclude:
- db/schema.rb
- bin/*
- node_modules/**/*
- Vagrantfile - Vagrantfile
- vendor/**/*
- config/initializers/json_ld* - config/initializers/json_ld*
- lib/mastodon/migration_helpers.rb - lib/mastodon/migration_helpers.rb
- lib/templates/**/*
ExtraDetails: true ExtraDetails: true
NewCops: enable NewCops: enable
TargetRubyVersion: 3.1 # Oldest supported ruby version TargetRubyVersion: 3.1 # Oldest supported ruby version
UseCache: true
inherit_from: inherit_from:
- .rubocop/layout.yml - .rubocop/layout.yml

View File

@ -1,14 +1,13 @@
--- ---
Rails/BulkChangeTable:
Enabled: false # Conflicts with strong_migrations features
Rails/FilePath: Rails/FilePath:
EnforcedStyle: arguments EnforcedStyle: arguments
Rails/HttpStatus: Rails/HttpStatus:
EnforcedStyle: numeric EnforcedStyle: numeric
Rails/LexicallyScopedActionFilter:
Exclude:
- app/controllers/auth/* # Conflicts with `Lint/UselessMethodDefinition` for inherited controller actions
Rails/NegateInclude: Rails/NegateInclude:
Enabled: false Enabled: false
@ -22,6 +21,3 @@ Rails/RakeEnvironment:
Rails/SkipsModelValidations: Rails/SkipsModelValidations:
Enabled: false Enabled: false
Rails/UnusedIgnoredColumns:
Enabled: false # Preserve ability to migrate from arbitrary old versions

View File

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.63.5. # using RuboCop version 1.65.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -14,7 +14,7 @@ Lint/NonLocalExitFromIterator:
Metrics/AbcSize: Metrics/AbcSize:
Max: 90 Max: 90
# Configuration parameters: CountBlocks, Max. # Configuration parameters: CountBlocks, CountModifierForms, Max.
Metrics/BlockNesting: Metrics/BlockNesting:
Exclude: Exclude:
- 'lib/tasks/mastodon.rake' - 'lib/tasks/mastodon.rake'
@ -31,14 +31,6 @@ Rails/OutputSafety:
Exclude: Exclude:
- 'config/initializers/simple_form.rb' - 'config/initializers/simple_form.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedMethods, AllowedPatterns.
# AllowedMethods: ==, equal?, eql?
Style/ClassEqualityComparison:
Exclude:
- 'app/helpers/jsonld_helper.rb'
- 'app/serializers/activitypub/outbox_serializer.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedVars. # Configuration parameters: AllowedVars.
Style/FetchEnvVar: Style/FetchEnvVar:
@ -48,14 +40,12 @@ Style/FetchEnvVar:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/2_limited_federation_mode.rb'
- 'config/initializers/3_omniauth.rb' - 'config/initializers/3_omniauth.rb'
- 'config/initializers/blacklists.rb'
- 'config/initializers/cache_buster.rb' - 'config/initializers/cache_buster.rb'
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/initializers/vapid.rb' - 'config/initializers/vapid.rb'
- 'lib/mastodon/redis_config.rb' - 'lib/mastodon/redis_config.rb'
- 'lib/tasks/repo.rake' - 'lib/tasks/repo.rake'
- 'spec/system/profile_spec.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. # Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns.
@ -66,40 +56,10 @@ Style/FormatStringToken:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'lib/paperclip/color_extractor.rb' - 'lib/paperclip/color_extractor.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream:
Exclude:
- 'config/environments/development.rb'
- 'config/environments/production.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
Style/GuardClause: Style/GuardClause:
Exclude: Enabled: false
- 'app/lib/activitypub/activity/block.rb'
- 'app/lib/request.rb'
- 'app/lib/request_pool.rb'
- 'app/lib/webfinger.rb'
- 'app/lib/webfinger_resource.rb'
- 'app/models/concerns/account/counters.rb'
- 'app/models/concerns/user/ldap_authenticable.rb'
- 'app/models/tag.rb'
- 'app/models/user.rb'
- 'app/services/fan_out_on_write_service.rb'
- 'app/services/post_status_service.rb'
- 'app/services/process_hashtags_service.rb'
- 'app/workers/move_worker.rb'
- 'app/workers/redownload_avatar_worker.rb'
- 'app/workers/redownload_header_worker.rb'
- 'app/workers/redownload_media_worker.rb'
- 'app/workers/remote_account_refresh_worker.rb'
- 'config/initializers/devise.rb'
- 'lib/devise/strategies/two_factor_ldap_authenticatable.rb'
- 'lib/devise/strategies/two_factor_pam_authenticatable.rb'
- 'lib/mastodon/cli/accounts.rb'
- 'lib/mastodon/cli/maintenance.rb'
- 'lib/mastodon/cli/media.rb'
- 'lib/tasks/repo.rake'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Style/HashTransformValues: Style/HashTransformValues:
@ -121,16 +81,10 @@ Style/MutableConstant:
- 'app/services/delete_account_service.rb' - 'app/services/delete_account_service.rb'
- 'lib/mastodon/migration_warning.rb' - 'lib/mastodon/migration_warning.rb'
# This cop supports safe autocorrection (--autocorrect).
Style/NilLambda:
Exclude:
- 'config/initializers/paperclip.rb'
# Configuration parameters: AllowedMethods. # Configuration parameters: AllowedMethods.
# AllowedMethods: respond_to_missing? # AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter: Style/OptionalBooleanParameter:
Exclude: Exclude:
- 'app/helpers/admin/account_moderation_notes_helper.rb'
- 'app/helpers/jsonld_helper.rb' - 'app/helpers/jsonld_helper.rb'
- 'app/lib/admin/system_check/message.rb' - 'app/lib/admin/system_check/message.rb'
- 'app/lib/request.rb' - 'app/lib/request.rb'
@ -154,13 +108,6 @@ Style/RedundantConstantBase:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/sidekiq.rb' - 'config/initializers/sidekiq.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
# AllowedMethods: present?, blank?, presence, try, try!
Style/SafeNavigation:
Exclude:
- 'app/models/concerns/account/finder_concern.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: WordRegex. # Configuration parameters: WordRegex.
# SupportedStyles: percent, brackets # SupportedStyles: percent, brackets

View File

@ -1 +1 @@
3.3.3 3.3.4

View File

@ -2,6 +2,365 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.3.0] - UNRELEASED
The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by @mjankowski.
### Security
- **Add confirmation interstitial instead of silently redirecting logged-out visitors to remote resources** (#27792, #28902, and #30651 by @ClearlyClaire and @Gargron)\
This fixes a longstanding open redirect in Mastodon, at the cost of added friction when local links to remote resources are shared.
### Added
- **Add experimental server-side notification grouping** (#29889, #30576, #30685, #30688, #30707, #30776, #30779, #30781, #30440, #31062, #31098, #31076, #31111, #31123, #31223, #31214, #31224, #31299, #31325, #31347, #31304, #31326, #31384, #31403, #31433, #31509, #31486, and #31513 by @ClearlyClaire, @mgmn, and @renchap)\
Group notifications of the same type for the same target, so that your notifications no longer get cluttered by boost and favorite notifications as soon as a couple of your posts get traction.\
This is done server-side so that clients can efficiently get relevant groups without having to go through numerous pages of individual notifications.\
As part of this, the visual design of the entire notifications feature has been revamped.\
This feature is intended to eventually replace the existing notifications column, but for this first beta, users will have to enable it in the “Experimental features” section of the notifications column settings.\
The API is not final yet, but it consists of:
- a new `group_key` attribute to `Notification` entities
- `GET /api/v2_alpha/notifications`: https://docs.joinmastodon.org/methods/notifications_alpha/#get-grouped
- `GET /api/v2_alpha/notifications/:group_key`: https://docs.joinmastodon.org/methods/notifications_alpha/#get-notification-group
- `POST /api/v2_alpha/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/notifications_alpha/#dismiss-group
- `GET /api/v2_alpha/notifications/:unread_count`: https://docs.joinmastodon.org/methods/notifications_alpha/#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, and #31541 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.\
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\
This adds the following REST API endpoints:
- `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy
- `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications
- `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests
- `GET /api/v1/notifications/requests/:id`: https://docs.joinmastodon.org/methods/notifications/#get-one-request
- `POST /api/v1/notifications/requests/:id/accept`: https://docs.joinmastodon.org/methods/notifications/#accept-request
- `POST /api/v1/notifications/requests/:id/dismiss`: https://docs.joinmastodon.org/methods/notifications/#dismiss-request
- `POST /api/v1/notifications/requests/accept`: https://docs.joinmastodon.org/methods/notifications/#accept-multiple-requests
- `POST /api/v1/notifications/requests/dismiss`: https://docs.joinmastodon.org/methods/notifications/#dismiss-multiple-requests
- `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged
In addition, accepting one or more notification requests generates a new streaming event:
- `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed
- **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 [`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.
- **Add "system" theme setting (light/dark theme depending on user system preference)** (#29748, #29553, #29795, #29918, #30839, and #30861 by @nshki, @ErikUden, @mjankowski, @renchap, and @vmstan)\
Add a “system” theme that automatically switch between default dark and light themes depending on the user's system preferences.\
Also changes the default server theme to this new “system” theme so that automatic theme selection happens even when logged out.
- **Add timeline of public posts about a trending link** (#30381 and #30840 by @Gargron)\
You can now see public posts mentioning currently-trending articles from people who have opted into discovery features.\
This adds a new REST API endpoint: https://docs.joinmastodon.org/methods/timelines/#link
- **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, and #30846 by @Gargron)\
This adds a mechanism to [highlight the author of news articles](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/) shared on Mastodon.\
Articles hosted outside the fediverse can indicate a fediverse author with a meta tag:
```html
<meta name="fediverse:creator" content="username@domain" />
```
On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors\
Note that this feature is still work in progress and the tagging format and verification mechanisms may change in future releases.
- **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\
In addition to email notifications, also notify users of moderation actions or warnings against them directly within the app, so they are less likely to miss important communication from their moderators.\
This adds the `moderation_warning` notification type to the REST API and streaming, with a new [`moderation_warning` attribute](https://docs.joinmastodon.org/entities/Notification/#moderation_warning).
- **Add domain information to profiles in web UI** (#29602 by @Gargron)\
Clicking the domain of a user in their profile will now open a tooltip with a short explanation about servers and federation.
- Add ability to reorder uploaded media before posting in web UI (#28456 by @Gargron)
- Add moderation interface for searching hashtags (#30880 by @ThisIsMissEm)
- Add ability for admins to configure instance favicon and logo (#30040, #30208, #30259, #30375, #30734, #31016, and #30205 by @ClearlyClaire, @FawazFarid, @JasonPunyon, @mgmn, and @renchap)\
This is also exposed through the REST API: https://docs.joinmastodon.org/entities/Instance/#icon
- Add `api_versions` to `/api/v2/instance` (#31354 by @ClearlyClaire)\
Add API version number to make it easier for clients to detect compatible features going forward.\
See API documentation at https://docs.joinmastodon.org/entities/Instance/#api-versions
- Add recent audit log entries in federation moderation interface (#27386 by @ThisIsMissEm)
- Add profile setup to onboarding in web UI (#27829, #27876, and #28453 by @Gargron)
- Add prominent share/copy button on profiles in web UI (#27865 and #27889 by @ClearlyClaire and @Gargron)
- Add optional hints for server rules (#29539 and #29758 by @ClearlyClaire and @Gargron)\
Server rules can now be broken into a short rule name and a longer explanation of the rule.\
This adds a new [`hint` attribute](https://docs.joinmastodon.org/entities/Rule/#hint) to `Rule` entities in the REST API.
- Add support for PKCE in OAuth flow (#31129 by @ThisIsMissEm)
- Add CDN cache busting on media deletion (#31353 and #31414 by @ClearlyClaire and @tribela)
- Add the OAuth application used in local reports (#30539 by @ThisIsMissEm)
- Add hint to user that other remote statuses may be missing (#26910, #31387, and #31516 by @Gargron, @audiodude, and @renchap)
- Add lang attribute on preview card title (#31303 by @c960657)
- Add check for `Content-Length` in `ResponseWithLimitAdapter` (#31285 by @c960657)
- Add `Accept-Language` header to fetch preview cards in the server's default language (#31232 by @c960657)
- Add support for PKCE Extension in OmniAuth OIDC through the `OIDC_USE_PKCE` environment variable (#31131 by @ThisIsMissEm)
- Add API endpoints for unread notifications count (#31191 by @ClearlyClaire)\
This adds the following REST API endpoints:
- `GET /api/v1/notifications/unread_count`: https://docs.joinmastodon.org/methods/notifications/#unread-count
- Add `/` keyboard shortcut to focus the search field (#29921 by @ClearlyClaire)
- Add button to view the Hashtag on the instance from Hashtags in Moderation UI (#31533 by @ThisIsMissEm)
- Add list of pending releases directly in mail notifications for version updates (#29436 and #30035 by @ClearlyClaire)
- Add “Appeals” link under “Moderation” navigation category in moderation interface (#31071 by @ThisIsMissEm)
- Add badge on account card in report moderation interface when account is already suspended (#29592 by @ClearlyClaire)
- Add admin comments directly to the `admin/instances` page (#29240 by @tribela)
- Add ability to require approval when users sign up using specific email domains (#28468, #28732, #28607, and #28608 by @ClearlyClaire)
- Add banner for forwarded reports made by remote users about remote content (#27549 by @ClearlyClaire)
- Add support HTML ruby tags in remote posts for east-asian languages (#30897 by @ThisIsMissEm)
- Add link to manage warning presets in admin navigation (#26199 by @vmstan)
- Add volume saving/reuse to video player (#27488 by @thehydrogen)
- Add Elasticsearch index size, ffmpeg and ImageMagick versions to the admin dashboard (#27301, #30710, #31130, and #30845 by @vmstan)
- Add `MASTODON_SIDEKIQ_READY_FILENAME` environment variable to use a file for Sidekiq to signal it is ready to process jobs (#30971 and #30988 by @renchap)\
In the official Docker image, this is set to `sidekiq_process_has_started_and_will_begin_processing_jobs` so that Sidekiq will touch `tmp/sidekiq_process_has_started_and_will_begin_processing_jobs` to signal readiness.
- Add `S3_RETRY_LIMIT` environment variable to make S3 retries configurable (#23215 by @smiba)
- Add `S3_KEY_PREFIX` environment variable (#30181 by @S0yKaf)
- Add support for multiple `redirect_uris` when creating OAuth 2.0 Applications (#29192 by @ThisIsMissEm)
- Add Interlingue and Interlingua to interface languages (#28630 and #30828 by @Dhghomon and @renchap)
- Add Kashubian, Pennsylvania Dutch, Vai, Jawi Malay, Mohawk and Low German to posting languages (#26024, #26634, #27136, #29098, #27115, and #27434 by @EngineerDali, @HelgeKrueger, and @gunchleoc)
- Add validations to `Web::PushSubscription` (#30540 and #30542 by @ThisIsMissEm)
- Add option to use native Ruby driver for Redis through `REDIS_DRIVER=ruby` (#30717 by @vmstan)
- Add support for libvips in addition to ImageMagick (#30090, #30590, #30597, #30632, #30857, #30869, and #30858 by @ClearlyClaire, @Gargron, and @mjankowski)\
Server admins can now use libvips as a faster and lighter alternative to ImageMagick for processing user-uploaded images.\
This requires libvips 8.13 or newer, and needs to be enabled with `MASTODON_USE_LIBVIPS=true`.\
This is enabled by default in the official Docker images, and is intended to completely replace ImageMagick in the future.
- Add active animation to header settings button (#30221, #30307, and #30388 by @daudix)
- Add OpenTelemetry instrumentation (#30130, #30322, #30353, and #30350 by @julianocosta89, @renchap, and @robbkidd)\
See https://docs.joinmastodon.org/admin/config/#otel for documentation
- Add API to get multiple accounts and statuses (#27871 and #30465 by @ClearlyClaire)\
This adds `GET /api/v1/accounts` and `GET /api/v1/statuses` to the REST API, see https://docs.joinmastodon.org/methods/accounts/#index and https://docs.joinmastodon.org/methods/statuses/#index
- Add redirection back to previous page after site upload deletion (#30141 by @FawazFarid)
- Add RFC8414 OAuth 2.0 server metadata (#29191 by @ThisIsMissEm)
- Add loading indicator and empty result message to advanced interface search (#30085 by @ClearlyClaire)
- Add `profile` OAuth 2.0 scope, allowing more limited access to user data (#29087 and #30357 by @ThisIsMissEm)
- Add the role ID to the badge component (#29707 by @renchap)
- Add diagnostic message for failure during CLI search deploy (#29462 by @mjankowski)
- Add pagination `Link` headers on API accounts/statuses when pinned true (#29442 by @mjankowski)
- Add support for specifying custom CA cert for Elasticsearch through `ES_CA_FILE` (#29122 and #29147 by @ClearlyClaire)
- Add groundwork for annual reports for accounts (#28693 by @Gargron)\
This lays the groundwork for a “year-in-review”/“wrapped” style report for local users, but is currently not in use.
- Add notification email on invalid second authenticator (#28822 by @ClearlyClaire)
- Add new emojis from `jdecked/twemoji` 15.0 (#28404 by @TheEssem)
- Add configurable error handling in attachment batch deletion (#28184 by @vmstan)\
This makes the S3 batch size configurable through the `S3_BATCH_DELETE_LIMIT` environment variable (defaults to 1000), and adds some retry logic, configurable through the `S3_BATCH_DELETE_RETRY` environment variable (defaults to 3).
- Add VAPID public key to instance serializer (#28006 by @ThisIsMissEm)
- Add `nodeName` and `nodeDescription` to nodeinfo `metadata` (#28079 by @6543)
- Add Thai diacritics and tone marks in `HASHTAG_INVALID_CHARS_RE` (#26576 by @ppnplus)
- Add variable delay before link verification of remote account links (#27774 by @ClearlyClaire)
- Add support for invite codes in the registration API (#27805 by @ClearlyClaire)
- Add HTML lang attribute to preview card descriptions (#27503 by @srapilly)
- Add display of relevant account warnings to report action logs (#27425 by @ClearlyClaire)
- Add validation of allowed schemes on preview card URLs (#27485 by @mjankowski)
- Add token introspection without read scope to `/api/v1/apps/verify_credentials` (#27142 by @ThisIsMissEm)
- Add support for cross-origin request to `/nodeinfo/2.0` (#27413 by @palant)
- Add variable delay before link verification of remote account links (#27351 by @ClearlyClaire)
- Add PWA shortcut to `/explore` page (#27235 by @jake-anto)
### Changed
- **Change icons throughout the web interface** (#27385, #27539, #27555, #27579, #27700, #27817, #28519, #28709, #28064, #28775, #28780, #27924, #29294, #29395, #29537, #29569, #29610, #29612, #29649, #29844, #27780, #30974, #30963, #30962, #30961, #31362, #31363, #31359, #31371, #31360, #31512, #31511, and #31525 by @ClearlyClaire, @Gargron, @arbolitoloco1, @mjankowski, @nclm, @renchap, @ronilaukkarinen, and @zunda)\
This changes all the interface icons from FontAwesome to Material Symbols for a more modern look, consistent with the official Mastodon Android app.\
In addition, better care is given to pixel alignment, and icon variants are used to better highlight active/inactive state.
- **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, and #29659 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\
The compose form has been completely redesigned for a more modern and consistent look, as well as spelling out the chosen privacy setting and language name at all times.\
As part of this, the “Unlisted” privacy setting has been renamed to “Quiet public”.
- **Change design of confirmation modals in the web UI** (#29576, #29614, #29640, #29644, #30131, #30884, and #31399 by @ClearlyClaire, @Gargron, and @tribela)\
The mute, block, and domain block confirmation modals have been completely redesigned to be clearer and include more detailed information on the action to be performed.\
They also have a more modern and consistent design, along with other confirmation modals in the application.
- **Change colors throughout the web UI** (#29522, #29584, #29653, #29779, #29803, #29809, #29808, #29828, #31034, #31168, #31266, #31348, #31349, #31361, and #31510 by @ClearlyClaire, @Gargron, @renchap, and @vmstan)
- **Change onboarding prompt to follow suggestions carousel in web UI** (#28878 and #29272 by @Gargron)
- **Change email templates** (#28416, #28755, #28814, #29064, #28883, #29470, #29607, #29761, #29760, and #29879 by @ClearlyClaire, @Gargron, @hteumeuleu, and @mjankowski)\
All emails to end-users have been completely redesigned with a fresh new look, providing more information while making them easier to read and keeping maximum compatibility across mail clients.
- **Change follow recommendations algorithm** (#28314, #28433, #29017, #29108, #29306, #29550, #29619, and #31474 by @ClearlyClaire, @Gargron, @kernal053, @mjankowski, and @wheatear-dev)\
This replaces the “past interactions” recommendation algorithm with a “friends of friends” algorithm that suggests accounts followed by people you follow, and a “similar profiles” algorithm that suggests accounts with a profile similar to your most recent follows.\
In addition, the implementation has been significantly reworked, and all follow recommendations are now dismissable.\
This change deprecates the `source` attribute in `Suggestion` entities in the REST API, and replaces it with the new [`sources` attribute](https://docs.joinmastodon.org/entities/Suggestion/#sources).
- Change account search algorithm (#30803 by @Gargron)
- **Change streaming server to use its own dependencies and its own docker image** (#24702, #27967, #26850, #28112, #28115, #28137, #28138, #28497, #28548, and #30795 by @TheEssem, @ThisIsMissEm, @jippi, @timetinytim, and @vmstan)\
In order to reduce the amount of runtime dependencies, the streaming server has been moved into a separate package and Docker image.\
The `mastodon` image does not contain the streaming server anymore, as it has been moved to its own `mastodon-streaming` image.\
Administrators may need to update their setup accordingly.
- Change how content warnings and filters are displayed in web UI (#31365 by @Gargron)
- Change Web UI to allow viewing and severing relationships with suspended accounts (#27667 by @ClearlyClaire)\
This also adds a `with_suspended` parameter to `GET /api/v1/accounts/relationships` in the REST API.
- Change avatars border radius (#31390 by @renchap)
- Change counters to be displayed on profile timelines in web UI (#30525 by @Gargron)
- Change disabled buttons color in light mode to make the difference more visible (#30998 by @renchap)
- Change design of people tab on explore in web UI (#30059 by @Gargron)
- Change sidebar text in web UI (#30696 by @Gargron)
- Change "Follow" to "Follow back" and "Mutual" when appropriate in web UI (#28452 and #28465 by @Gargron and @renchap)
- Change media to be hidden/blurred by default in report modal (#28522 by @ClearlyClaire)
- Change order of the "muting" and "blocking" list options in “Data Exports” (#26088 by @fixermark)
- Change admin and moderation notes character limit from 500 to 2000 characters (#30288 by @ThisIsMissEm)
- Change mute options to be in dropdown on muted users list in web UI (#30049 and #31315 by @ClearlyClaire and @Gargron)
- Change out-of-band hashtags design in web UI (#29732 by @Gargron)
- Change design of metadata underneath detailed posts in web UI (#29585, #29605, and #29648 by @ClearlyClaire and @Gargron)
- Change action button to be last on profiles in web UI (#29533 and #29923 by @ClearlyClaire and @Gargron)
- Change confirmation prompts in trending moderation interface to be more specific (#19626 by @tribela)
- Change “Trends” moderation menu to “Recommendations & Trends” and move follow recommendations there (#31292 by @ThisIsMissEm)
- Change irrelevant fields in account cleanup settings to be disabled unless automatic cleanup is enabled (#26562 by @c960657)
- Change dropdown menu icon to not be replaced by close icon when open in web UI (#29532 by @Gargron)
- Change back button to always appear in advanced web UI (#29551 and #29669 by @Gargron)
- Change border of active compose field search inputs (#29832 and #29839 by @vmstan)
- Change link detection to allow `@` at the end of an URL (#31124 by @adamniedzielski)
- Change User-Agent to use Mastodon as the product, and http.rb as platform details (#31192 by @ClearlyClaire)
- Change layout and wording of the Content Retention server settings page (#27733 by @vmstan)
- Change unconfirmed users to be kept for one week instead of two days (#30285 by @renchap)
- Change maximum page size for Admin Domain Management APIs from 200 to 500 (#31253 by @ThisIsMissEm)
- Change database pool size to default to Sidekiq concurrency settings in Sidekiq processes (#26488 by @sinoru)
- Change alt text to empty string for avatars (#21875 by @jasminjohal)
- Change Docker images to use custom-built libvips and ffmpeg (#30571, #30569, and #31498 by @vmstan)
- Change external links in the admin audit log to plain text or local administration pages (#27139 and #27150 by @ClearlyClaire and @ThisIsMissEm)
- Change YJIT to be enabled when available (#30310 and #27283 by @ClearlyClaire and @mjankowski)\
Enable Ruby's built-in just-in-time compiler. This improves performances substantially, at the cost of a slightly increased memory usage.
- Change `.env` file loading from deprecated `dotenv-rails` gem to `dotenv` gem (#29173 and #30121 by @mjankowski)\
This should have no effect except in the unlikely case an environment variable included a newline.
- Change “Panjabi” language name to the more common spelling “Punjabi” (#27117 by @gunchleoc)
- Change encryption of OTP secrets to use ActiveRecord Encryption (#29831, #28325, #30151, #30202, #30340, and #30344 by @ClearlyClaire and @mjankowski)\
This requires a manual step from administrators of existing servers. Indeed, they need to generate new secrets, which can be done using `bundle exec rails db:encryption:init`.\
Furthermore, there is a risk that the introduced migration fails if the server was misconfigured in the past. If that happens, the migration error will include the relevant information.
- Change `/api/v1/announcements` to return regular `Status` entities (#26736 by @ClearlyClaire)
- Change imports to convert case-insensitive fields to lowercase (#29739 and #29740 by @ThisIsMissEm)
- Change stats in the admin interface to be inclusive of the full selected range, from beginning of day to end of day (#29416 and #29841 by @mjankowski)
- Change materialized views to be refreshed concurrently to avoid locks (#29015 by @Gargron)
- Change compose form to use server-provided post character and poll options limits (#28928 and #29490 by @ClearlyClaire and @renchap)
- Change streaming server logging from `npmlog` to `pino` and `pino-http` (#27828 by @ThisIsMissEm)\
This changes the Mastodon streaming server log format, so this might be considered a breaking change if you were parsing the logs.
- Change media “ALT” label to use a specific CSS class (#28777 by @ClearlyClaire)
- Change streaming API host to not be overridden to localhost in development mode (#28557 by @ClearlyClaire)
- Change cookie rotator to use SHA1 digest for new cookies (#27392 by @ClearlyClaire)\
Note that this requires that no pre-4.2.0 Mastodon web server is running when this code is deployed, as those would not understand the new cookies.\
Therefore, zero-downtime updates are only supported if you're coming from 4.2.0 or newer. If you want to skip Mastodon 4.2, you will need to completely stop Mastodon services before updating.
- Change preview card deletes to be done using batch method (#28183 by @vmstan)
- Change `img-src` and `media-src` CSP directives to not include `https:` (#28025 and #28561 by @ClearlyClaire)
- Change self-destruct procedure (#26439, #29049, and #29420 by @ClearlyClaire and @zunda)\
Instead of enqueuing deletion jobs immediately, `tootctl self-destruct` now outputs a value for the `SELF_DESTRUCT` environment variable, which puts a server in self-destruct mode, processing deletions in the background, while giving users access to their export archives.
### Removed
- Remove StatsD integration (replaced by OpenTelemetry) (#30240 by @mjankowski)
- Remove `CacheBuster` default options (#30718 by @mjankowski)
- Remove home marker updates from the Web UI (#22721 by @davbeck)\
The web interface was unconditionally updating the home marker to the most recent received post, discarding any value set by other clients, thus making the feature unreliable.
- Remove support for Ruby 3.0 (reaching EOL) (#29702 by @mjankowski)
- Remove setting for unfollow confirmation modal (#29373 by @ClearlyClaire)\
Instead, the unfollow confirmation modal will always be displayed.
- Remove support for Capistrano (#27295 and #30009 by @mjankowski and @renchap)
### Fixed
- **Fix link preview cards not always preserving the original URL from the status** (#27312 by @Gargron)
- Fix log out from user menu not working on Safari (#31402 by @renchap)
- Fix various issues when in link preview card generation (#28748, #30017, #30362, #30173, #30853, #30929, #30933, #30957, #30987, and #31144 by @adamniedzielski, @oneiros, @phocks, @timothyjrogers, and @tribela)
- Fix handling of missing links in Webfinger responses (#31030 by @adamniedzielski)
- Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil)
- Fix cross-origin loading of `inert.css` polyfill (#30687 by @louis77)
- Fix cutoff of instance name in sign-up form (#30598 by @oneiros)
- Fix empty `aria-hidden` attribute value in logo resources area (#30570 by @mjankowski)
- Fix “Redirect URI” field not being marked as required in “New application” form (#30311 by @ThisIsMissEm)
- Fix right-to-left text in preview cards (#30930 by @ClearlyClaire)
- Fix rack attack `match_type` value typo in logging config (#30514 by @mjankowski)
- Fix various cases of duplicate, missing, or inconsistent borders or scrollbar styles (#31068, #31286, #31268, #31275, #31284, #31305, #31346, #31372, #31373, #31389, #31432, #31391, and #31445 by @valtlai and @vmstan)
- Fix race condition in `POST /api/v1/push/subscription` (#30166 by @ClearlyClaire)
- Fix post deletion not being delayed when those are part of an account warning (#30163 by @ClearlyClaire)
- Fix rendering error on `/start` when not logged in (#30023 by @timothyjrogers)
- Fix logo pushing header buttons out of view on certain conditions in mobile layout (#29787 by @ClearlyClaire)
- Fix notification-related records not being reattributed when merging accounts (#29694 by @ClearlyClaire)
- Fix results/query in `api/v1/featured_tags/suggestions` (#29597 by @mjankowski)
- Fix distracting and confusing always-showing scrollbar track in boost confirmation modal (#31524 by @ClearlyClaire)
- Fix being able to upload more than 4 media attachments in some cases (#29183 by @mashirozx)
- Fix preview card player getting embedded when clicking on the external link button (#29457 by @ClearlyClaire)
- Fix full date display not respecting the locale 12/24h format (#29448 by @renchap)
- Fix filters title and keywords overflow (#29396 by @GeopJr)
- Fix incorrect date format in “Follows and followers” (#29390 by @JasonPunyon)
- Fix “Edit media” modal sizing and layout when space-constrained (#27095 by @ronilaukkarinen)
- Fix modal container bounds (#29185 by @nico3333fr)
- Fix inefficient HTTP signature parsing using regexps and `StringScanner` (#29133 by @ClearlyClaire)
- Fix moderation report updates through `PUT /api/v1/admin/reports/:id` not being logged in the audit log (#29044, #30342, and #31033 by @mjankowski, @tribela, and @vmstan)
- Fix moderation interface allowing to select rule violation when there are no server rules (#31458 by @ThisIsMissEm)
- Fix redirection from paths with url-encoded `@` to their decoded form (#31184 by @timothyjrogers)
- Fix Trending Tags pending review having an unstable sort order (#31473 by @ThisIsMissEm)
- Fix the emoji dropdown button always opening the dropdown instead of behaving like a toggle (#29012 by @jh97uk)
- Fix processing of incoming posts with bearcaps (#26527 by @kmycode)
- Fix support for IPv6 redis connections in streaming (#31229 by @ThisIsMissEm)
- Fix search form re-rendering spuriously in web UI (#28876 by @Gargron)
- Fix `RedownloadMediaWorker` not being called on transient S3 failure (#28714 by @ClearlyClaire)
- Fix ISO code for Canadian French from incorrect `fr-QC` to `fr-CA` (#26015 by @gunchleoc)
- Fix `.opus` file uploads being misidentified by Paperclip (#28580 by @vmstan)
- Fix loading local accounts with extraneous domain part in WebUI (#28559 by @ClearlyClaire)
- Fix destructive actions in dropdowns not using error color in light theme (#28484 by @logicalmoody)
- Fix call to inefficient `delete_matched` cache method in domain blocks (#28374 by @ClearlyClaire)
- Fix status edits not always being streamed to mentioned users (#28324 by @ClearlyClaire)
- Fix onboarding step descriptions being truncated on narrow screens (#28021 by @ClearlyClaire)
- Fix duplicate IDs in relationships and familiar_followers APIs (#27982 by @KevinBongart)
- Fix modal content not being selectable (#27813 by @pajowu)
- Fix Web UI not displaying appropriate explanation when a user hides their follows/followers (#27791 by @ClearlyClaire)
- Fix format-dependent redirects being cached regardless of requested format (#27632 by @ClearlyClaire)
- Fix confusing screen when visiting a confirmation link for an already-confirmed email (#27368 by @ClearlyClaire)
- Fix explore page reloading when you navigate back to it in web UI (#27489 by @Gargron)
- Fix missing redirection from `/home` to `/deck/home` in the advanced interface (#27378 by @Signez)
- 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
### Added
- Add support for incoming `<s>` tag ([mediaformat](https://github.com/mastodon/mastodon/pull/31375))
### Changed
- Change logic of block/mute bypass for mentions from moderators to only apply to visible roles with moderation powers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31271))
### Fixed
- Fix incorrect rate limit on PUT requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31356))
- Fix presence of `ß` in adjacent word preventing mention and hashtag matching ([adamniedzielski](https://github.com/mastodon/mastodon/pull/31122))
- Fix processing of webfinger responses with multiple `self` links ([adamniedzielski](https://github.com/mastodon/mastodon/pull/31110))
- Fix duplicate `orderedItems` in user archive's `outbox.json` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31099))
- Fix click event handling when clicking outside of an open dropdown menu ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31251))
- Fix status processing failing halfway when a remote post has a malformed `replies` attribute ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/31246))
- Fix `--verbose` option of `tootctl media remove`, which was previously erroneously removed ([mjankowski](https://github.com/mastodon/mastodon/pull/30536))
- Fix division by zero on some video/GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30600))
- Fix Web UI trying to save user settings despite being logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30324))
- Fix hashtag regexp matching some link anchors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30190))
- Fix local account search on LDAP login being case-sensitive ([raucao](https://github.com/mastodon/mastodon/pull/30113))
- Fix development environment admin account not being auto-approved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29958))
- Fix report reason selector in moderation interface not unselecting rules when changing category ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29026))
- Fix already-invalid reports failing to resolve ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29027))
- Fix OCR when using S3/CDN for assets ([vmstan](https://github.com/mastodon/mastodon/pull/28551))
- Fix error when encountering malformed `Tag` objects from Kbin ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28235))
- Fix not all allowed image formats showing in file picker when uploading custom emoji ([june128](https://github.com/mastodon/mastodon/pull/28076))
- Fix search popout listing unusable search options when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27918))
- Fix processing of featured collections lacking an `items` attribute ([tribela](https://github.com/mastodon/mastodon/pull/27581))
- Fix `mastodon:stats` decoration of stats rake task ([mjankowski](https://github.com/mastodon/mastodon/pull/31104))
## [4.2.10] - 2024-07-04
### Security
- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7))
- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3))
- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx))
- Update dependencies
### Added
- Add yarn version specification to avoid confusion with Yarn 3 and Yarn 4
### Changed
- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854))
- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865))
- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691))
- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377))
### Removed
- Removed dependency on `posix-spawn` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18559))
### Fixed
- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584))
- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780))
- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819))
- Fix duplicate `@context` attribute in user archive export ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30653))
## [4.2.9] - 2024-05-30 ## [4.2.9] - 2024-05-30
### Security ### Security

View File

@ -50,6 +50,11 @@ You can contribute in the following ways:
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
Please review the org-level [contribution guidelines] for high-level acceptance
criteria guidance.
[contribution guidelines]: https://github.com/mastodon/.github/blob/main/CONTRIBUTING.md
## API Changes and Additions ## API Changes and Additions
Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation). Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation).

View File

@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.7 # syntax=docker/dockerfile:1.9
# This file is designed for production server deployment, not local development work # 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/README.md#docker # For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker
@ -12,22 +12,22 @@ ARG BUILDPLATFORM=${BUILDPLATFORM}
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"] # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
# renovate: datasource=docker depName=docker.io/ruby # renovate: datasource=docker depName=docker.io/ruby
ARG RUBY_VERSION="3.3.3" ARG RUBY_VERSION="3.3.4"
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
# renovate: datasource=node-version depName=node # renovate: datasource=node-version depName=node
ARG NODE_MAJOR_VERSION="20" ARG NODE_MAJOR_VERSION="20"
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
ARG DEBIAN_VERSION="bookworm" ARG DEBIAN_VERSION="bookworm"
# Node image to use for base image based on combined variables (ex: 20-bookworm-slim) # 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 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) # 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 FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
# Example: v4.2.0-nightly.2023.11.09+something # Example: v4.3.0-nightly.2023.11.09+pr-123456
# Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] # Overwrite existence of 'alpha.X' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
ARG MASTODON_VERSION_PRERELEASE="" ARG MASTODON_VERSION_PRERELEASE=""
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-12345"] # Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"]
ARG MASTODON_VERSION_METADATA="" ARG MASTODON_VERSION_METADATA=""
# Allow Ruby on Rails to serve static files # Allow Ruby on Rails to serve static files
@ -67,7 +67,9 @@ ENV \
# Optimize jemalloc 5.x performance # Optimize jemalloc 5.x performance
MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \ MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \
# Enable libvips, should not be changed # Enable libvips, should not be changed
MASTODON_USE_LIBVIPS=true MASTODON_USE_LIBVIPS=true \
# Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes
MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs
# Set default shell used for running commands # Set default shell used for running commands
SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"]
@ -100,9 +102,7 @@ RUN \
apt-get dist-upgrade -yq; \ apt-get dist-upgrade -yq; \
# Install jemalloc, curl and other necessary components # Install jemalloc, curl and other necessary components
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
ca-certificates \
curl \ curl \
ffmpeg \
file \ file \
libjemalloc2 \ libjemalloc2 \
patchelf \ patchelf \
@ -119,7 +119,7 @@ RUN \
; ;
# Create temporary build layer from base image # Create temporary build layer from base image
FROM ruby as build FROM ruby AS build
# Copy Node package configuration files into working directory # Copy Node package configuration files into working directory
COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/
@ -137,7 +137,10 @@ RUN \
--mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \
# Install build tools and bundler dependencies from APT # Install build tools and bundler dependencies from APT
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \
autoconf \
automake \
build-essential \ build-essential \
cmake \
git \ git \
libgdbm-dev \ libgdbm-dev \
libglib2.0-dev \ libglib2.0-dev \
@ -146,9 +149,12 @@ RUN \
libidn-dev \ libidn-dev \
libpq-dev \ libpq-dev \
libssl-dev \ libssl-dev \
libtool \
meson \ meson \
nasm \
pkg-config \ pkg-config \
shared-mime-info \ shared-mime-info \
xz-utils \
# libvips components # libvips components
libcgif-dev \ libcgif-dev \
libexif-dev \ libexif-dev \
@ -162,6 +168,16 @@ RUN \
libspng-dev \ libspng-dev \
libtiff-dev \ libtiff-dev \
libwebp-dev \ libwebp-dev \
# ffmpeg components
libdav1d-dev \
liblzma-dev \
libmp3lame-dev \
libopus-dev \
libsnappy-dev \
libvorbis-dev \
libvpx-dev \
libx264-dev \
libx265-dev \
; ;
RUN \ RUN \
@ -171,27 +187,75 @@ RUN \
corepack prepare --activate; corepack prepare --activate;
# Create temporary libvips specific build layer from build layer # Create temporary libvips specific build layer from build layer
FROM build as libvips FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
ARG VIPS_VERSION=8.15.2 ARG VIPS_VERSION=8.15.3
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] # 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 ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
WORKDIR /usr/local/libvips/src WORKDIR /usr/local/libvips/src
# Download and extract libvips source code
ADD ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz /usr/local/libvips/src/
RUN tar xf vips-${VIPS_VERSION}.tar.xz;
WORKDIR /usr/local/libvips/src/vips-${VIPS_VERSION}
# Configure and compile libvips
RUN \ RUN \
curl -sSL -o vips-${VIPS_VERSION}.tar.xz ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz; \
tar xf vips-${VIPS_VERSION}.tar.xz; \
cd vips-${VIPS_VERSION}; \
meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \ meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \
cd build; \ cd build; \
ninja; \ ninja; \
ninja install; ninja install;
# Create temporary ffmpeg specific build layer from build layer
FROM build AS ffmpeg
# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"]
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
ARG FFMPEG_VERSION=7.0.2
# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"]
ARG FFMPEG_URL=https://ffmpeg.org/releases
WORKDIR /usr/local/ffmpeg/src
# Download and extract ffmpeg source code
ADD ${FFMPEG_URL}/ffmpeg-${FFMPEG_VERSION}.tar.xz /usr/local/ffmpeg/src/
RUN tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz;
WORKDIR /usr/local/ffmpeg/src/ffmpeg-${FFMPEG_VERSION}
# Configure and compile ffmpeg
RUN \
./configure \
--prefix=/usr/local/ffmpeg \
--toolchain=hardened \
--disable-debug \
--disable-devices \
--disable-doc \
--disable-ffplay \
--disable-network \
--disable-static \
--enable-ffmpeg \
--enable-ffprobe \
--enable-gpl \
--enable-libdav1d \
--enable-libmp3lame \
--enable-libopus \
--enable-libsnappy \
--enable-libvorbis \
--enable-libvpx \
--enable-libwebp \
--enable-libx264 \
--enable-libx265 \
--enable-shared \
--enable-version3 \
; \
make -j$(nproc); \
make install;
# Create temporary bundler specific build layer from build layer # Create temporary bundler specific build layer from build layer
FROM build as bundler FROM build AS bundler
ARG TARGETPLATFORM ARG TARGETPLATFORM
@ -213,7 +277,7 @@ RUN \
bundle install -j"$(nproc)"; bundle install -j"$(nproc)";
# Create temporary node specific build layer from build layer # Create temporary node specific build layer from build layer
FROM build as yarn FROM build AS yarn
ARG TARGETPLATFORM ARG TARGETPLATFORM
@ -230,7 +294,7 @@ RUN \
yarn workspaces focus --production @mastodon/mastodon; yarn workspaces focus --production @mastodon/mastodon;
# Create temporary assets build layer from build layer # Create temporary assets build layer from build layer
FROM build as precompiler FROM build AS precompiler
# Copy Mastodon sources into precompiler layer # Copy Mastodon sources into precompiler layer
COPY . /opt/mastodon/ COPY . /opt/mastodon/
@ -254,7 +318,7 @@ RUN \
rm -fr /opt/mastodon/tmp; rm -fr /opt/mastodon/tmp;
# Prep final Mastodon Ruby layer # Prep final Mastodon Ruby layer
FROM ruby as mastodon FROM ruby AS mastodon
ARG TARGETPLATFORM ARG TARGETPLATFORM
@ -289,6 +353,20 @@ RUN \
libwebp7 \ libwebp7 \
libwebpdemux2 \ libwebpdemux2 \
libwebpmux3 \ libwebpmux3 \
# ffmpeg components
libdav1d6 \
libmp3lame0 \
libopencore-amrnb0 \
libopencore-amrwb0 \
libopus0 \
libsnappy1v5 \
libtheora0 \
libvorbis0a \
libvorbisenc2 \
libvorbisfile3 \
libvpx7 \
libx264-164 \
libx265-199 \
; ;
# Copy Mastodon sources into final layer # Copy Mastodon sources into final layer
@ -302,11 +380,16 @@ COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/
# Copy libvips components to layer # Copy libvips components to layer
COPY --from=libvips /usr/local/libvips/bin /usr/local/bin COPY --from=libvips /usr/local/libvips/bin /usr/local/bin
COPY --from=libvips /usr/local/libvips/lib /usr/local/lib COPY --from=libvips /usr/local/libvips/lib /usr/local/lib
# Copy ffpmeg components to layer
COPY --from=ffmpeg /usr/local/ffmpeg/bin /usr/local/bin
COPY --from=ffmpeg /usr/local/ffmpeg/lib /usr/local/lib
RUN \ RUN \
ldconfig; \ ldconfig; \
# Smoketest media processors # Smoketest media processors
vips -v; vips -v; \
ffmpeg -version; \
ffprobe -version;
RUN \ RUN \
# Precompile bootsnap code for faster Rails startup # Precompile bootsnap code for faster Rails startup

26
Gemfile
View File

@ -9,9 +9,6 @@ gem 'rack', '~> 2.2.7'
gem 'rails', '~> 7.1.1' gem 'rails', '~> 7.1.1'
gem 'thor', '~> 1.2' gem 'thor', '~> 1.2'
# For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182
gem 'irb', '~> 1.8'
gem 'dotenv' gem 'dotenv'
gem 'haml-rails', '~>2.0' gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.5' gem 'pg', '~> 1.5'
@ -19,7 +16,7 @@ gem 'pghero'
gem 'aws-sdk-s3', '~> 1.123', require: false gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
gem 'fog-core', '<= 2.4.0' gem 'fog-core', '<= 2.5.0'
gem 'fog-openstack', '~> 1.0', require: false gem 'fog-openstack', '~> 1.0', require: false
gem 'kt-paperclip', '~> 7.2' gem 'kt-paperclip', '~> 7.2'
gem 'md-paperclip-azure', '~> 2.2', require: false gem 'md-paperclip-azure', '~> 2.2', require: false
@ -28,7 +25,7 @@ gem 'ruby-vips', '~> 2.2', require: false
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8' gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.18.0', require: false gem 'bootsnap', '~> 1.18.0', require: false
gem 'browser' gem 'browser', '< 6' # https://github.com/fnando/browser/issues/543
gem 'charlock_holmes', '~> 0.7.7' gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3' gem 'chewy', '~> 7.3'
gem 'devise', '~> 4.9' gem 'devise', '~> 4.9'
@ -61,17 +58,17 @@ gem 'httplog', '~> 1.7.0'
gem 'i18n' gem 'i18n'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'inline_svg' gem 'inline_svg'
gem 'irb', '~> 1.8'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15' gem 'nokogiri', '~> 1.15'
gem 'nsa'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'
gem 'premailer-rails' gem 'premailer-rails'
gem 'public_suffix', '~> 5.0' gem 'public_suffix', '~> 6.0'
gem 'pundit', '~> 2.3' gem 'pundit', '~> 2.3'
gem 'rack-attack', '~> 6.6' gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rack-cors', '~> 2.0', require: 'rack/cors'
@ -90,7 +87,7 @@ gem 'sidekiq-unique-jobs', '~> 7.1'
gem 'simple_form', '~> 5.2' gem 'simple_form', '~> 5.2'
gem 'simple-navigation', '~> 4.4' gem 'simple-navigation', '~> 4.4'
gem 'stoplight', '~> 4.1' gem 'stoplight', '~> 4.1'
gem 'strong_migrations', '1.8.0' gem 'strong_migrations'
gem 'tty-prompt', '~> 0.23', require: false gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0' gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023' gem 'tzinfo-data', '~> 1.2023'
@ -102,12 +99,10 @@ gem 'json-ld'
gem 'json-ld-preloaded', '~> 3.2' gem 'json-ld-preloaded', '~> 3.2'
gem 'rdf-normalize', '~> 0.5' gem 'rdf-normalize', '~> 0.5'
gem 'private_address_check', '~> 0.5' gem 'opentelemetry-api', '~> 1.3.0'
gem 'opentelemetry-api', '~> 1.2.5'
group :opentelemetry do group :opentelemetry do
gem 'opentelemetry-exporter-otlp', '~> 0.27.0', require: false gem 'opentelemetry-exporter-otlp', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', 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-active_model_serializers', '~> 0.20.1', require: false
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false
@ -116,15 +111,18 @@ group :opentelemetry do
gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false gem 'opentelemetry-instrumentation-http', '~> 0.23.2', require: false
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', 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-net_http', '~> 0.22.4', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.27.1', require: false gem 'opentelemetry-instrumentation-pg', '~> 0.28.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.30.0', require: false gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
gem 'opentelemetry-sdk', '~> 1.4', require: false gem 'opentelemetry-sdk', '~> 1.4', require: false
end end
group :test do group :test do
# Enable usage of all available CPUs/cores during spec runs
gem 'flatware-rspec'
# Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab
gem 'rspec-github', '~> 2.4', require: false gem 'rspec-github', '~> 2.4', require: false

View File

@ -89,8 +89,8 @@ GEM
minitest (>= 5.1) minitest (>= 5.1)
mutex_m mutex_m
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.6) addressable (2.8.7)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0) aes_key_wrap (1.1.0)
android_key_attestation (0.3.0) android_key_attestation (0.3.0)
annotate (3.2.0) annotate (3.2.0)
@ -100,20 +100,20 @@ GEM
attr_required (1.0.2) attr_required (1.0.2)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.940.0) aws-partitions (1.966.0)
aws-sdk-core (3.197.0) aws-sdk-core (3.201.5)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.83.0) aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.197.0) aws-sdk-core (~> 3, >= 3.201.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.152.3) aws-sdk-s3 (1.159.0)
aws-sdk-core (~> 3, >= 3.197.0) aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.5)
aws-sigv4 (1.8.0) aws-sigv4 (1.9.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@ -135,16 +135,16 @@ GEM
binding_of_caller (1.0.1) binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0) debug_inspector (>= 1.2.0)
blurhash (0.1.7) blurhash (0.1.7)
bootsnap (1.18.3) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (6.1.2) brakeman (6.2.1)
racc racc
browser (5.3.1) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
redis (>= 1.0, < 6) redis (>= 1.0, < 6)
builder (3.2.4) builder (3.3.0)
bundler-audit (0.9.1) bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3) bundler (>= 1.2.0, < 3)
thor (~> 1.0) thor (~> 1.0)
capybara (3.40.0) capybara (3.40.0)
@ -159,7 +159,7 @@ GEM
case_transform (0.2) case_transform (0.2)
activesupport activesupport
cbor (0.5.9.8) cbor (0.5.9.8)
charlock_holmes (0.7.7) charlock_holmes (0.7.9)
chewy (7.6.0) chewy (7.6.0)
activesupport (>= 5.2) activesupport (>= 5.2)
elasticsearch (>= 7.14.0, < 8) elasticsearch (>= 7.14.0, < 8)
@ -168,7 +168,7 @@ GEM
climate_control (1.2.0) climate_control (1.2.0)
cocoon (1.2.15) cocoon (1.2.15)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.3.3) concurrent-ruby (1.3.4)
connection_pool (2.4.1) connection_pool (2.4.1)
cose (1.3.0) cose (1.3.0)
cbor (~> 0.5.9) cbor (~> 0.5.9)
@ -180,7 +180,7 @@ GEM
css_parser (1.17.1) css_parser (1.17.1)
addressable addressable
csv (3.3.0) csv (3.3.0)
database_cleaner-active_record (2.1.0) database_cleaner-active_record (2.2.0)
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
@ -195,7 +195,7 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (5.0.0) devise-two-factor (5.1.0)
activesupport (~> 7.0) activesupport (~> 7.0)
devise (~> 4.0) devise (~> 4.0)
railties (~> 7.0) railties (~> 7.0)
@ -208,7 +208,7 @@ GEM
activerecord (>= 4.2, < 8) activerecord (>= 4.2, < 8)
docile (1.4.0) docile (1.4.0)
domain_name (0.6.20240107) domain_name (0.6.20240107)
doorkeeper (5.6.9) doorkeeper (5.7.1)
railties (>= 5) railties (>= 5)
dotenv (3.1.2) dotenv (3.1.2)
drb (2.2.1) drb (2.2.1)
@ -222,16 +222,16 @@ GEM
elasticsearch-transport (7.17.10) elasticsearch-transport (7.17.10)
faraday (>= 1, < 3) faraday (>= 1, < 3)
multi_json multi_json
email_spec (2.2.2) email_spec (2.3.0)
htmlentities (~> 4.3.3) htmlentities (~> 4.3.3)
launchy (~> 2.1) launchy (>= 2.1, < 4.0)
mail (~> 2.7) mail (~> 2.7)
erubi (1.12.0) erubi (1.13.0)
et-orbi (1.2.11) et-orbi (1.2.11)
tzinfo tzinfo
excon (0.110.0) excon (0.111.0)
fabrication (2.31.0) fabrication (2.31.0)
faker (3.4.1) faker (3.4.2)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@ -264,7 +264,12 @@ GEM
ffi-compiler (1.3.2) ffi-compiler (1.3.2)
ffi (>= 1.15.5) ffi (>= 1.15.5)
rake rake
fog-core (2.4.0) flatware (2.3.2)
thor (< 2.0)
flatware-rspec (2.3.2)
flatware (= 2.3.2)
rspec (>= 3.6)
fog-core (2.5.0)
builder builder
excon (~> 0.71) excon (~> 0.71)
formatador (>= 0.2, < 2.0) formatador (>= 0.2, < 2.0)
@ -276,15 +281,15 @@ GEM
fog-core (~> 2.1) fog-core (~> 2.1)
fog-json (>= 1.0) fog-json (>= 1.0)
formatador (1.1.0) formatador (1.1.0)
fugit (1.10.1) fugit (1.11.1)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4) raabro (~> 1.4)
fuubar (2.5.1) fuubar (2.5.1)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-protobuf (3.25.3) google-protobuf (3.25.4)
googleapis-common-protos-types (1.14.0) googleapis-common-protos-types (1.14.0)
google-protobuf (~> 3.18) google-protobuf (~> 3.18)
haml (6.3.0) haml (6.3.0)
@ -341,7 +346,7 @@ GEM
activesupport (>= 3.0) activesupport (>= 3.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
io-console (0.7.2) io-console (0.7.2)
irb (1.13.2) irb (1.14.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
@ -352,17 +357,18 @@ GEM
aes_key_wrap aes_key_wrap
bindata bindata
httpclient httpclient
json-ld (3.3.1) json-ld (3.3.2)
htmlentities (~> 4.3) htmlentities (~> 4.3)
json-canonicalization (~> 1.0) json-canonicalization (~> 1.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.15) multi_json (~> 1.15)
rack (>= 2.2, < 4) rack (>= 2.2, < 4)
rdf (~> 3.3) rdf (~> 3.3)
rexml (~> 3.2)
json-ld-preloaded (3.3.0) json-ld-preloaded (3.3.0)
json-ld (~> 3.3) json-ld (~> 3.3)
rdf (~> 3.3) rdf (~> 3.3)
json-schema (4.3.0) json-schema (4.3.1)
addressable (>= 2.8) addressable (>= 2.8)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.7.1) jwt (2.7.1)
@ -398,6 +404,7 @@ GEM
llhttp-ffi (0.5.0) llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
logger (1.6.0)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@ -422,10 +429,10 @@ GEM
memory_profiler (1.0.2) memory_profiler (1.0.2)
mime-types (3.5.2) mime-types (3.5.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2024.0604) mime-types-data (3.2024.0702)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.7)
minitest (5.23.1) minitest (5.25.1)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.0) multipart-post (2.4.0)
@ -434,7 +441,7 @@ GEM
uri uri
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.12) net-imap (0.4.14)
date date
net-protocol net-protocol
net-ldap (0.19.0) net-ldap (0.19.0)
@ -445,16 +452,12 @@ GEM
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.3) nio4r (2.7.3)
nokogiri (1.16.6) nokogiri (1.16.7)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nsa (0.3.0) oj (3.16.5)
activesupport (>= 4.2, < 7.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.4)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (2.1.2) omniauth (2.1.2)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
@ -463,7 +466,7 @@ GEM
addressable (~> 2.8) addressable (~> 2.8)
nokogiri (~> 1.12) nokogiri (~> 1.12)
omniauth (~> 2.1) omniauth (~> 2.1)
omniauth-rails_csrf_protection (1.0.1) omniauth-rails_csrf_protection (1.0.2)
actionpack (>= 4.2) actionpack (>= 4.2)
omniauth (~> 2.0) omniauth (~> 2.0)
omniauth-saml (2.1.0) omniauth-saml (2.1.0)
@ -486,11 +489,11 @@ GEM
openssl (3.2.0) openssl (3.2.0)
openssl-signature_algorithm (1.3.0) openssl-signature_algorithm (1.3.0)
openssl (> 2.0) openssl (> 2.0)
opentelemetry-api (1.2.5) opentelemetry-api (1.3.0)
opentelemetry-common (0.20.1) opentelemetry-common (0.20.1)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-exporter-otlp (0.27.0) opentelemetry-exporter-otlp (0.28.1)
google-protobuf (~> 3.14) google-protobuf (>= 3.18)
googleapis-common-protos-types (~> 1.3) googleapis-common-protos-types (~> 1.3)
opentelemetry-api (~> 1.1) opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20) opentelemetry-common (~> 0.20)
@ -506,93 +509,87 @@ GEM
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (~> 0.21) opentelemetry-instrumentation-rack (~> 0.21)
opentelemetry-instrumentation-action_view (0.7.0) opentelemetry-instrumentation-action_view (0.7.2)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1) opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_job (0.7.1) opentelemetry-instrumentation-active_job (0.7.7)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_model_serializers (0.20.1) opentelemetry-instrumentation-active_model_serializers (0.20.2)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_record (0.7.2) opentelemetry-instrumentation-active_record (0.7.3)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_support (0.5.1) opentelemetry-instrumentation-active_support (0.6.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-base (0.22.3) opentelemetry-instrumentation-base (0.22.3)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-registry (~> 0.1) opentelemetry-registry (~> 0.1)
opentelemetry-instrumentation-concurrent_ruby (0.21.3) opentelemetry-instrumentation-concurrent_ruby (0.21.4)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-excon (0.22.1) opentelemetry-instrumentation-excon (0.22.4)
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-faraday (0.24.2)
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http (0.23.3)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-http_client (0.22.4) opentelemetry-instrumentation-faraday (0.24.6)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-net_http (0.22.4) opentelemetry-instrumentation-http (0.23.4)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-pg (0.27.3) opentelemetry-instrumentation-http_client (0.22.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-net_http (0.22.7)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-pg (0.28.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (0.24.3) opentelemetry-instrumentation-rack (0.24.6)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rails (0.30.2) opentelemetry-instrumentation-rails (0.31.2)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-action_mailer (~> 0.1.0) opentelemetry-instrumentation-action_mailer (~> 0.1.0)
opentelemetry-instrumentation-action_pack (~> 0.9.0) opentelemetry-instrumentation-action_pack (~> 0.9.0)
opentelemetry-instrumentation-action_view (~> 0.7.0) opentelemetry-instrumentation-action_view (~> 0.7.0)
opentelemetry-instrumentation-active_job (~> 0.7.0) opentelemetry-instrumentation-active_job (~> 0.7.0)
opentelemetry-instrumentation-active_record (~> 0.7.0) opentelemetry-instrumentation-active_record (~> 0.7.0)
opentelemetry-instrumentation-active_support (~> 0.5.0) opentelemetry-instrumentation-active_support (~> 0.6.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-redis (0.25.4) opentelemetry-instrumentation-redis (0.25.7)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-sidekiq (0.25.3) opentelemetry-instrumentation-sidekiq (0.25.7)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-registry (0.3.1) opentelemetry-registry (0.3.1)
opentelemetry-api (~> 1.1) opentelemetry-api (~> 1.1)
opentelemetry-sdk (1.4.1) opentelemetry-sdk (1.5.0)
opentelemetry-api (~> 1.1) opentelemetry-api (~> 1.1)
opentelemetry-common (~> 0.20) opentelemetry-common (~> 0.20)
opentelemetry-registry (~> 0.2) opentelemetry-registry (~> 0.2)
opentelemetry-semantic_conventions opentelemetry-semantic_conventions
opentelemetry-semantic_conventions (1.10.0) opentelemetry-semantic_conventions (1.10.1)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostruct (0.6.0)
ox (2.14.18) ox (2.14.18)
parallel (1.25.1) parallel (1.25.1)
parser (3.3.3.0) parser (3.3.4.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.6) pg (1.5.7)
pghero (3.5.0) pghero (3.6.0)
activerecord (>= 6) activerecord (>= 6.1)
premailer (1.23.0) premailer (1.23.0)
addressable addressable
css_parser (>= 1.12.0) css_parser (>= 1.12.0)
@ -601,21 +598,20 @@ GEM
actionmailer (>= 3) actionmailer (>= 3)
net-smtp net-smtp
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0) propshaft (0.9.1)
propshaft (0.9.0)
actionpack (>= 7.0.0) actionpack (>= 7.0.0)
activesupport (>= 7.0.0) activesupport (>= 7.0.0)
rack rack
railties (>= 7.0.0) railties (>= 7.0.0)
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (5.1.1) public_suffix (6.0.1)
puma (6.4.2) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.2) pundit (2.3.2)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.0) racc (1.8.1)
rack (2.2.9) rack (2.2.9)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
@ -677,12 +673,13 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rdf (3.3.1) rdf (3.3.2)
bcp47_spec (~> 0.2) bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.7.0) rdf-normalize (0.7.0)
rdf (~> 3.3) rdf (~> 3.3)
rdoc (6.6.3.1) rdoc (6.7.0)
psych (>= 4.0.0) psych (>= 4.0.0)
redcarpet (3.6.0) redcarpet (3.6.0)
redis (4.8.1) redis (4.8.1)
@ -698,7 +695,7 @@ GEM
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.3.0) rexml (3.3.6)
strscan strscan
rotp (6.3.0) rotp (6.3.0)
rouge (4.2.1) rouge (4.2.1)
@ -707,9 +704,13 @@ GEM
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-core (3.13.0) rspec-core (3.13.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.0) rspec-expectations (3.13.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-github (2.4.0) rspec-github (2.4.0)
@ -717,7 +718,7 @@ GEM
rspec-mocks (3.13.1) rspec-mocks (3.13.1)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (6.1.2) rspec-rails (6.1.4)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
@ -731,13 +732,13 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 8) sidekiq (>= 5, < 8)
rspec-support (3.13.1) rspec-support (3.13.1)
rubocop (1.64.1) rubocop (1.65.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 2.4, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
@ -749,12 +750,12 @@ GEM
rubocop-performance (1.21.1) rubocop-performance (1.21.1)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.25.0) rubocop-rails (2.25.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.1) rubocop-rspec (3.0.4)
rubocop (~> 1.61) rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0) rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61) rubocop (~> 1.61)
@ -764,22 +765,24 @@ GEM
ruby-saml (1.16.0) ruby-saml (1.16.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
rexml rexml
ruby-vips (2.2.1) ruby-vips (2.2.2)
ffi (~> 1.12) ffi (~> 1.12)
logger
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
rufus-scheduler (3.9.1) rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0) safety_net_attestation (0.4.0)
jwt (~> 2.0) jwt (~> 2.0)
sanitize (6.1.1) sanitize (6.1.3)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
scenic (1.8.0) scenic (1.8.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 4.0.0) railties (>= 4.0.0)
selenium-webdriver (4.21.1) selenium-webdriver (4.23.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0) rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0) websocket (~> 1.0)
@ -790,10 +793,10 @@ GEM
redis (>= 4.5.0, < 5) redis (>= 4.5.0, < 5)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
sidekiq sidekiq
sidekiq-scheduler (5.0.3) sidekiq-scheduler (5.0.6)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 6, < 8)
tilt (>= 1.4.0) tilt (>= 1.4.0, < 3)
sidekiq-unique-jobs (7.1.33) sidekiq-unique-jobs (7.1.33)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0) brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
@ -813,12 +816,11 @@ GEM
simplecov-lcov (0.8.0) simplecov-lcov (0.8.0)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
stackprof (0.2.26) stackprof (0.2.26)
statsd-ruby (1.5.0)
stoplight (4.1.0) stoplight (4.1.0)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.1.1) stringio (3.1.1)
strong_migrations (1.8.0) strong_migrations (2.0.0)
activerecord (>= 5.2) activerecord (>= 6.1)
strscan (3.1.0) strscan (3.1.0)
swd (1.3.0) swd (1.3.0)
activesupport (>= 3) activesupport (>= 3)
@ -830,7 +832,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1) terrapin (1.0.1)
climate_control climate_control
test-prof (1.3.3) test-prof (1.4.1)
thor (1.3.1) thor (1.3.1)
tilt (2.3.0) tilt (2.3.0)
timeout (0.4.1) timeout (0.4.1)
@ -890,7 +892,7 @@ GEM
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
webrick (1.8.1) webrick (1.8.1)
websocket (1.2.10) websocket (1.2.11)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
@ -898,7 +900,7 @@ GEM
xorcist (1.1.3) xorcist (1.1.3)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.15) zeitwerk (2.6.17)
PLATFORMS PLATFORMS
ruby ruby
@ -913,7 +915,7 @@ DEPENDENCIES
blurhash (~> 0.1) blurhash (~> 0.1)
bootsnap (~> 1.18.0) bootsnap (~> 1.18.0)
brakeman (~> 6.0) brakeman (~> 6.0)
browser browser (< 6)
bundler-audit (~> 0.9) bundler-audit (~> 0.9)
capybara (~> 3.39) capybara (~> 3.39)
charlock_holmes (~> 0.7.7) charlock_holmes (~> 0.7.7)
@ -938,7 +940,8 @@ DEPENDENCIES
faker (~> 3.2) faker (~> 3.2)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fastimage fastimage
fog-core (<= 2.4.0) flatware-rspec
fog-core (<= 2.5.0)
fog-openstack (~> 1.0) fog-openstack (~> 1.0)
fuubar (~> 2.5) fuubar (~> 2.5)
haml-rails (~> 2.0) haml-rails (~> 2.0)
@ -971,15 +974,14 @@ DEPENDENCIES
net-http (~> 0.4.0) net-http (~> 0.4.0)
net-ldap (~> 0.18) net-ldap (~> 0.18)
nokogiri (~> 1.15) nokogiri (~> 1.15)
nsa
oj (~> 3.14) oj (~> 3.14)
omniauth (~> 2.0) omniauth (~> 2.0)
omniauth-cas (~> 3.0.0.beta.1) omniauth-cas (~> 3.0.0.beta.1)
omniauth-rails_csrf_protection (~> 1.0) omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.0) omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1) omniauth_openid_connect (~> 0.6.1)
opentelemetry-api (~> 1.2.5) opentelemetry-api (~> 1.3.0)
opentelemetry-exporter-otlp (~> 0.27.0) opentelemetry-exporter-otlp (~> 0.28.0)
opentelemetry-instrumentation-active_job (~> 0.7.1) opentelemetry-instrumentation-active_job (~> 0.7.1)
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)
opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2)
@ -988,9 +990,9 @@ DEPENDENCIES
opentelemetry-instrumentation-http (~> 0.23.2) opentelemetry-instrumentation-http (~> 0.23.2)
opentelemetry-instrumentation-http_client (~> 0.22.3) opentelemetry-instrumentation-http_client (~> 0.22.3)
opentelemetry-instrumentation-net_http (~> 0.22.4) opentelemetry-instrumentation-net_http (~> 0.22.4)
opentelemetry-instrumentation-pg (~> 0.27.1) opentelemetry-instrumentation-pg (~> 0.28.0)
opentelemetry-instrumentation-rack (~> 0.24.1) opentelemetry-instrumentation-rack (~> 0.24.1)
opentelemetry-instrumentation-rails (~> 0.30.0) opentelemetry-instrumentation-rails (~> 0.31.0)
opentelemetry-instrumentation-redis (~> 0.25.3) opentelemetry-instrumentation-redis (~> 0.25.3)
opentelemetry-instrumentation-sidekiq (~> 0.25.2) opentelemetry-instrumentation-sidekiq (~> 0.25.2)
opentelemetry-sdk (~> 1.4) opentelemetry-sdk (~> 1.4)
@ -999,9 +1001,8 @@ DEPENDENCIES
pg (~> 1.5) pg (~> 1.5)
pghero pghero
premailer-rails premailer-rails
private_address_check (~> 0.5)
propshaft propshaft
public_suffix (~> 5.0) public_suffix (~> 6.0)
puma (~> 6.3) puma (~> 6.3)
pundit (~> 2.3) pundit (~> 2.3)
rack (~> 2.2.7) rack (~> 2.2.7)
@ -1042,7 +1043,7 @@ DEPENDENCIES
simplecov-lcov (~> 0.8) simplecov-lcov (~> 0.8)
stackprof stackprof
stoplight (~> 4.1) stoplight (~> 4.1)
strong_migrations (= 1.8.0) strong_migrations
test-prof test-prof
thor (~> 1.2) thor (~> 1.2)
tty-prompt (~> 0.23) tty-prompt (~> 0.23)

View File

@ -13,6 +13,7 @@ module Admin
def show def show
authorize :instance, :show? authorize :instance, :show?
@time_period = (6.days.ago.to_date...Time.now.utc.to_date) @time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
end end
def destroy def destroy

View File

@ -2,7 +2,15 @@
module Admin module Admin
class TagsController < BaseController class TagsController < BaseController
before_action :set_tag before_action :set_tag, except: [:index]
PER_PAGE = 20
def index
authorize :tag, :index?
@tags = filtered_tags.page(params[:page]).per(PER_PAGE)
end
def show def show
authorize @tag, :show? authorize @tag, :show?
@ -31,5 +39,13 @@ module Admin
def tag_params def tag_params
params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable) params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable)
end end
def filtered_tags
TagFilter.new(filter_params.with_defaults(order: 'newest')).results
end
def filter_params
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
end
end end
end end

View File

@ -30,10 +30,10 @@ class Api::BaseController < ApplicationController
protected protected
def limit_param(default_limit) def limit_param(default_limit, max_limit = nil)
return default_limit unless params[:limit] return default_limit unless params[:limit]
[params[:limit].to_i.abs, default_limit * 2].min [params[:limit].to_i.abs, max_limit || (default_limit * 2)].min
end end
def params_slice(*keys) def params_slice(*keys)

View File

@ -5,6 +5,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
include AccountableConcern include AccountableConcern
LIMIT = 100 LIMIT = 100
MAX_LIMIT = 500
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show]
@ -47,18 +48,13 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
private private
def set_domain_allows def set_domain_allows
@domain_allows = filtered_domain_allows.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) @domain_allows = DomainAllow.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT, MAX_LIMIT), params_slice(:max_id, :since_id, :min_id))
end end
def set_domain_allow def set_domain_allow
@domain_allow = DomainAllow.find(params[:id]) @domain_allow = DomainAllow.find(params[:id])
end end
def filtered_domain_allows
# TODO: no filtering yet
DomainAllow.all
end
def next_path def next_path
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue? api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end end
@ -72,7 +68,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
end end
def records_continue? def records_continue?
@domain_allows.size == limit_param(LIMIT) @domain_allows.size == limit_param(LIMIT, MAX_LIMIT)
end end
def resource_params def resource_params

View File

@ -5,6 +5,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
include AccountableConcern include AccountableConcern
LIMIT = 100 LIMIT = 100
MAX_LIMIT = 500
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show] before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show]
@ -59,18 +60,13 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
end end
def set_domain_blocks def set_domain_blocks
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) @domain_blocks = DomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT, MAX_LIMIT), params_slice(:max_id, :since_id, :min_id))
end end
def set_domain_block def set_domain_block
@domain_block = DomainBlock.find(params[:id]) @domain_block = DomainBlock.find(params[:id])
end end
def filtered_domain_blocks
# TODO: no filtering yet
DomainBlock.all
end
def domain_block_params def domain_block_params
params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
end end
@ -88,7 +84,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
end end
def records_continue? def records_continue?
@domain_blocks.size == limit_param(LIMIT) @domain_blocks.size == limit_param(LIMIT, MAX_LIMIT)
end end
def resource_params def resource_params

View File

@ -8,12 +8,12 @@ class Api::V1::Notifications::PoliciesController < Api::BaseController
before_action :set_policy before_action :set_policy
def show def show
render json: @policy, serializer: REST::NotificationPolicySerializer render json: @policy, serializer: REST::V1::NotificationPolicySerializer
end end
def update def update
@policy.update!(resource_params) @policy.update!(resource_params)
render json: @policy, serializer: REST::NotificationPolicySerializer render json: @policy, serializer: REST::V1::NotificationPolicySerializer
end end
private private

View File

@ -1,11 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Notifications::RequestsController < Api::BaseController class Api::V1::Notifications::RequestsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :index include Redisable
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: [:index, :show, :merged?]
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: [:index, :show, :merged?]
before_action :require_user! before_action :require_user!
before_action :set_request, except: :index before_action :set_request, only: [:show, :accept, :dismiss]
before_action :set_requests, only: [:accept_bulk, :dismiss_bulk]
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
@ -18,6 +21,10 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships
end end
def merged?
render json: { merged: redis.get("notification_unfilter_jobs:#{current_account.id}").to_i <= 0 }
end
def show def show
render json: @request, serializer: REST::NotificationRequestSerializer render json: @request, serializer: REST::NotificationRequestSerializer
end end
@ -28,14 +35,24 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
end end
def dismiss def dismiss
@request.update!(dismissed: true) DismissNotificationRequestService.new.call(@request)
render_empty
end
def accept_bulk
@requests.each { |request| AcceptNotificationRequestService.new.call(request) }
render_empty
end
def dismiss_bulk
@requests.each(&:destroy!)
render_empty render_empty
end end
private private
def load_requests def load_requests
requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed) || false).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id( requests = NotificationRequest.where(account: current_account).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),
params_slice(:max_id, :since_id, :min_id) params_slice(:max_id, :since_id, :min_id)
) )
@ -53,14 +70,22 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
@request = NotificationRequest.where(account: current_account).find(params[:id]) @request = NotificationRequest.where(account: current_account).find(params[:id])
end end
def set_requests
@requests = NotificationRequest.where(account: current_account, id: Array(params[:id]).uniq.map(&:to_i))
end
def next_path def next_path
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty? api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) if records_continue?
end end
def prev_path def prev_path
api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty? api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty?
end end
def records_continue?
@requests.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_max_id def pagination_max_id
@requests.last.id @requests.last.id
end end
@ -68,8 +93,4 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
def pagination_since_id def pagination_since_id
@requests.first.id @requests.first.id
end end
def pagination_params(core_params)
params.slice(:dismissed).permit(:dismissed).merge(core_params)
end
end end

View File

@ -7,6 +7,8 @@ class Api::V1::NotificationsController < Api::BaseController
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
DEFAULT_NOTIFICATIONS_LIMIT = 40 DEFAULT_NOTIFICATIONS_LIMIT = 40
DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100
MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000
def index def index
with_read_replica do with_read_replica do
@ -17,6 +19,14 @@ class Api::V1::NotificationsController < Api::BaseController
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
end end
def unread_count
limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT)
with_read_replica do
render json: { count: browserable_account_notifications.paginate_by_min_id(limit, notification_marker&.last_read_id).count }
end
end
def show def show
@notification = current_account.notifications.without_suspended.find(params[:id]) @notification = current_account.notifications.without_suspended.find(params[:id])
render json: @notification, serializer: REST::NotificationSerializer render json: @notification, serializer: REST::NotificationSerializer
@ -63,6 +73,10 @@ class Api::V1::NotificationsController < Api::BaseController
) )
end end
def notification_marker
current_user.markers.find_by(timeline: 'notifications')
end
def target_statuses_from_notifications def target_statuses_from_notifications
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status) @notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
end end

View File

@ -8,7 +8,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
before_action :set_poll before_action :set_poll
def create def create
VoteService.new.call(current_account, @poll, vote_params[:choices]) VoteService.new.call(current_account, @poll, vote_params)
render json: @poll, serializer: REST::PollSerializer render json: @poll, serializer: REST::PollSerializer
end end
@ -22,6 +22,6 @@ class Api::V1::Polls::VotesController < Api::BaseController
end end
def vote_params def vote_params
params.permit(choices: []) params.require(:choices)
end end
end end

View File

@ -10,7 +10,7 @@ class Api::V1::ReportsController < Api::BaseController
@report = ReportService.new.call( @report = ReportService.new.call(
current_account, current_account,
reported_account, reported_account,
report_params report_params.merge(application: doorkeeper_token.application)
) )
render json: @report, serializer: REST::ReportSerializer render json: @report, serializer: REST::ReportSerializer

View File

@ -6,6 +6,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy] before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy]
before_action :require_user!
before_action :set_statuses, only: :index before_action :set_statuses, only: :index
before_action :set_status, except: :index before_action :set_status, except: :index

View File

@ -2,6 +2,7 @@
class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' } before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
before_action :require_user!
before_action :set_translation before_action :set_translation
rescue_from TranslationService::NotConfiguredError, with: :not_found rescue_from TranslationService::NotConfiguredError, with: :not_found

View File

@ -3,8 +3,14 @@
class Api::V1::Timelines::BaseController < Api::BaseController class Api::V1::Timelines::BaseController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
before_action :require_user!, if: :require_auth?
private private
def require_auth?
!Setting.timeline_preview
end
def pagination_collection def pagination_collection
@statuses @statuses
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth? before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :set_preview_card before_action :set_preview_card
before_action :set_statuses before_action :set_statuses
@ -17,10 +17,6 @@ class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController
private private
def require_auth?
!Setting.timeline_preview
end
def set_preview_card def set_preview_card
@preview_card = PreviewCard.joins(:trend).merge(PreviewCardTrend.allowed).find_by!(url: params[:url]) @preview_card = PreviewCard.joins(:trend).merge(PreviewCardTrend.allowed).find_by!(url: params[:url])
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
before_action :require_user!, only: [:show], if: :require_auth? before_action -> { authorize_if_got_token! :read, :'read:statuses' }
PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze
@ -13,10 +13,6 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController
private private
def require_auth?
!Setting.timeline_preview
end
def load_statuses def load_statuses
preloaded_public_statuses_page preloaded_public_statuses_page
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth? before_action -> { authorize_if_got_token! :read, :'read:statuses' }
before_action :load_tag before_action :load_tag
PERMITTED_PARAMS = %i(local limit only_media).freeze PERMITTED_PARAMS = %i(local limit only_media).freeze

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
class Api::V2::Notifications::PoliciesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :show
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: :update
before_action :require_user!
before_action :set_policy
def show
render json: @policy, serializer: REST::NotificationPolicySerializer
end
def update
@policy.update!(resource_params)
render json: @policy, serializer: REST::NotificationPolicySerializer
end
private
def set_policy
@policy = NotificationPolicy.find_or_initialize_by(account: current_account)
with_read_replica do
@policy.summarize!
end
end
def resource_params
params.permit(
:for_not_following,
:for_not_followers,
:for_new_accounts,
:for_private_mentions,
:for_limited_accounts
)
end
end

View File

@ -7,20 +7,49 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
DEFAULT_NOTIFICATIONS_LIMIT = 40 DEFAULT_NOTIFICATIONS_LIMIT = 40
DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100
MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000
def index def index
with_read_replica do with_read_replica do
@notifications = load_notifications @notifications = load_notifications
@group_metadata = load_group_metadata @group_metadata = load_group_metadata
@grouped_notifications = load_grouped_notifications
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) @relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
@presenter = GroupedNotificationsPresenter.new(@grouped_notifications, expand_accounts: expand_accounts_param)
# Preload associations to avoid N+1s
ActiveRecord::Associations::Preloader.new(records: @presenter.accounts, associations: [:account_stat, { user: :role }]).call
end end
render json: @notifications.map { |notification| NotificationGroup.from_notification(notification) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#index rendering') do |span|
statuses = @grouped_notifications.filter_map { |group| group.target_status&.id }
span.add_attributes(
'app.notification_grouping.count' => @grouped_notifications.size,
'app.notification_grouping.account.count' => @presenter.accounts.size,
'app.notification_grouping.partial_account.count' => @presenter.partial_accounts.size,
'app.notification_grouping.status.count' => statuses.size,
'app.notification_grouping.status.unique_count' => statuses.uniq.size,
'app.notification_grouping.expand_accounts_param' => expand_accounts_param
)
render json: @presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata, expand_accounts: expand_accounts_param
end
end
def unread_count
limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT)
with_read_replica do
render json: { count: browserable_account_notifications.paginate_groups_by_min_id(limit, min_id: notification_marker&.last_read_id).count }
end
end end
def show def show
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id]) @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id])
render json: NotificationGroup.from_notification(@notification), serializer: REST::NotificationGroupSerializer presenter = GroupedNotificationsPresenter.new([NotificationGroup.from_notification(@notification)])
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
end end
def clear def clear
@ -36,25 +65,35 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
private private
def load_notifications def load_notifications
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id( MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_notifications') do
limit_param(DEFAULT_NOTIFICATIONS_LIMIT), notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
params_slice(:max_id, :since_id, :min_id) limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
) params_slice(:max_id, :since_id, :min_id)
)
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses| Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
preload_collection(target_statuses, Status) preload_collection(target_statuses, Status)
end
end end
end end
def load_group_metadata def load_group_metadata
return {} if @notifications.empty? return {} if @notifications.empty?
browserable_account_notifications MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_group_metadata') do
.where(group_key: @notifications.filter_map(&:group_key)) browserable_account_notifications
.where(id: (@notifications.last.id)..(@notifications.first.id)) .where(group_key: @notifications.filter_map(&:group_key))
.group(:group_key) .where(id: (@notifications.last.id)..(@notifications.first.id))
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at') .group(:group_key)
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] } .pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
end
end
def load_grouped_notifications
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do
@notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }
end
end end
def browserable_account_notifications def browserable_account_notifications
@ -65,6 +104,10 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
) )
end end
def notification_marker
current_user.markers.find_by(timeline: 'notifications')
end
def target_statuses_from_notifications def target_statuses_from_notifications
@notifications.filter_map(&:target_status) @notifications.filter_map(&:target_status)
end end
@ -88,4 +131,15 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :types, :exclude_types, :include_filtered).permit(:limit, :include_filtered, types: [], exclude_types: []).merge(core_params) params.slice(:limit, :types, :exclude_types, :include_filtered).permit(:limit, :include_filtered, types: [], exclude_types: []).merge(core_params)
end end
def expand_accounts_param
case params[:expand_accounts]
when nil, 'full'
'full'
when 'partial_avatars'
'partial_avatars'
else
raise Mastodon::InvalidParameterError, "Invalid value for 'expand_accounts': '#{params[:expand_accounts]}', allowed values are 'full' and 'partial_avatars'"
end
end
end end

View File

@ -23,7 +23,6 @@ class ApplicationController < ActionController::Base
helper_method :current_theme helper_method :current_theme
helper_method :single_user_mode? helper_method :single_user_mode?
helper_method :use_seamless_external_login? helper_method :use_seamless_external_login?
helper_method :omniauth_only?
helper_method :sso_account_settings helper_method :sso_account_settings
helper_method :limited_federation_mode? helper_method :limited_federation_mode?
helper_method :body_class_string helper_method :body_class_string
@ -140,10 +139,6 @@ class ApplicationController < ActionController::Base
Devise.pam_authentication || Devise.ldap_authentication Devise.pam_authentication || Devise.ldap_authentication
end end
def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end
def sso_account_settings def sso_account_settings
ENV.fetch('SSO_ACCOUNT_SETTINGS', nil) ENV.fetch('SSO_ACCOUNT_SETTINGS', nil)
end end

View File

@ -5,7 +5,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
layout 'auth' layout 'auth'
before_action :set_body_classes
before_action :set_confirmation_user!, only: [:show, :confirm_captcha] before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
before_action :redirect_confirmed_user, if: :signed_in_confirmed_user? before_action :redirect_confirmed_user, if: :signed_in_confirmed_user?
@ -73,10 +72,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
end end
def set_body_classes
@body_classes = 'lighter'
end
def after_resending_confirmation_instructions_path_for(_resource_name) def after_resending_confirmation_instructions_path_for(_resource_name)
if user_signed_in? if user_signed_in?
if current_user.confirmed? && current_user.approved? if current_user.confirmed? && current_user.approved?

View File

@ -3,7 +3,6 @@
class Auth::PasswordsController < Devise::PasswordsController class Auth::PasswordsController < Devise::PasswordsController
skip_before_action :check_self_destruct! skip_before_action :check_self_destruct!
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid? before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
before_action :set_body_classes
layout 'auth' layout 'auth'
@ -24,10 +23,6 @@ class Auth::PasswordsController < Devise::PasswordsController
redirect_to new_password_path(resource_name) redirect_to new_password_path(resource_name)
end end
def set_body_classes
@body_classes = 'lighter'
end
def reset_password_token_is_valid? def reset_password_token_is_valid?
resource_class.with_reset_password_token(params[:reset_password_token]).present? resource_class.with_reset_password_token(params[:reset_password_token]).present?
end end

View File

@ -25,6 +25,14 @@ class Auth::RegistrationsController < Devise::RegistrationsController
super(&:build_invite_request) super(&:build_invite_request)
end end
def edit # rubocop:disable Lint/UselessMethodDefinition
super
end
def create # rubocop:disable Lint/UselessMethodDefinition
super
end
def update def update
super do |resource| super do |resource|
resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password? resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password?
@ -97,7 +105,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
private private
def set_body_classes def set_body_classes
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter' @body_classes = 'admin' if %w(edit update).include?(action_name)
end end
def set_invite def set_invite

View File

@ -16,8 +16,6 @@ class Auth::SessionsController < Devise::SessionsController
include Auth::TwoFactorAuthenticationConcern include Auth::TwoFactorAuthenticationConcern
before_action :set_body_classes
content_security_policy only: :new do |p| content_security_policy only: :new do |p|
p.form_action(false) p.form_action(false)
end end
@ -103,10 +101,6 @@ class Auth::SessionsController < Devise::SessionsController
private private
def set_body_classes
@body_classes = 'lighter'
end
def home_paths(resource) def home_paths(resource)
paths = [about_path, '/explore'] paths = [about_path, '/explore']
@ -193,4 +187,15 @@ class Auth::SessionsController < Devise::SessionsController
def second_factor_attempts_key(user) def second_factor_attempts_key(user)
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}" "2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
end end
def respond_to_on_destroy
respond_to do |format|
format.json do
render json: {
redirect_to: after_sign_out_path_for(resource_name),
}, status: 200
end
format.all { super }
end
end
end end

View File

@ -5,7 +5,6 @@ class Auth::SetupController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :require_unconfirmed_or_pending! before_action :require_unconfirmed_or_pending!
before_action :set_body_classes
before_action :set_user before_action :set_user
skip_before_action :require_functional! skip_before_action :require_functional!
@ -35,10 +34,6 @@ class Auth::SetupController < ApplicationController
@user = current_user @user = current_user
end end
def set_body_classes
@body_classes = 'lighter'
end
def user_params def user_params
params.require(:user).permit(:email) params.require(:user).permit(:email)
end end

View File

@ -83,7 +83,6 @@ module Auth::TwoFactorAuthenticationConcern
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
register_attempt_in_session(user) register_attempt_in_session(user)
@body_classes = 'lighter'
@webauthn_enabled = user.webauthn_enabled? @webauthn_enabled = user.webauthn_enabled?
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank? @scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
'webauthn' 'webauthn'

View File

@ -42,7 +42,6 @@ module ChallengableConcern
end end
def render_challenge def render_challenge
@body_classes = 'lighter'
render 'auth/challenges/new', layout: 'auth' render 'auth/challenges/new', layout: 'auth'
end end

View File

@ -5,7 +5,6 @@ class MailSubscriptionsController < ApplicationController
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :set_body_classes
before_action :set_user before_action :set_user
before_action :set_type before_action :set_type
@ -25,10 +24,6 @@ class MailSubscriptionsController < ApplicationController
not_found unless @user not_found unless @user
end end
def set_body_classes
@body_classes = 'lighter'
end
def set_type def set_type
@type = email_type_from_param @type = email_type_from_param
end end

View File

@ -17,6 +17,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
def destroy def destroy
Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner) Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner)
Doorkeeper::Application.find_by(id: params[:id])&.close_streaming_sessions(current_resource_owner)
super super
end end

View File

@ -25,7 +25,7 @@ module Admin::FilterHelper
end end
def table_link_to(icon, text, path, **options) def table_link_to(icon, text, path, **options)
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link') link_to safe_join([material_symbol(icon), text]), path, options.merge(class: 'table-action-link')
end end
def selected?(more_params) def selected?(more_params)

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Admin::TagsHelper
def admin_tags_moderation_options
[
[t('admin.tags.moderation.reviewed'), 'reviewed'],
[t('admin.tags.moderation.review_requested'), 'review_requested'],
[t('admin.tags.moderation.unreviewed'), 'unreviewed'],
[t('admin.tags.moderation.trendable'), 'trendable'],
[t('admin.tags.moderation.not_trendable'), 'not_trendable'],
[t('admin.tags.moderation.usable'), 'usable'],
[t('admin.tags.moderation.not_usable'), 'not_usable'],
]
end
end

View File

@ -86,7 +86,7 @@ module ApplicationHelper
def html_title def html_title
safe_join( safe_join(
[content_for(:page_title).to_s.chomp, title] [content_for(:page_title).to_s.chomp, title]
.select(&:present?), .compact_blank,
' - ' ' - '
) )
end end
@ -105,19 +105,12 @@ module ApplicationHelper
policy(record).public_send(:"#{action}?") policy(record).public_send(:"#{action}?")
end end
def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split || []
class_names << 'fa'
class_names += icon.split.map { |cl| "fa-#{cl}" }
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
def material_symbol(icon, attributes = {}) def material_symbol(icon, attributes = {})
inline_svg_tag( inline_svg_tag(
"400-24px/#{icon}.svg", "400-24px/#{icon}.svg",
class: %w(icon).concat(attributes[:class].to_s.split), class: ['icon', "material-#{icon}"].concat(attributes[:class].to_s.split),
role: :img role: :img,
data: attributes[:data]
) )
end end
@ -127,23 +120,23 @@ module ApplicationHelper
def visibility_icon(status) def visibility_icon(status)
if status.public_visibility? if status.public_visibility?
fa_icon('globe', title: I18n.t('statuses.visibilities.public')) material_symbol('globe', title: I18n.t('statuses.visibilities.public'))
elsif status.unlisted_visibility? elsif status.unlisted_visibility?
fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted')) material_symbol('lock_open', title: I18n.t('statuses.visibilities.unlisted'))
elsif status.private_visibility? || status.limited_visibility? elsif status.private_visibility? || status.limited_visibility?
fa_icon('lock', title: I18n.t('statuses.visibilities.private')) material_symbol('lock', title: I18n.t('statuses.visibilities.private'))
elsif status.direct_visibility? elsif status.direct_visibility?
fa_icon('at', title: I18n.t('statuses.visibilities.direct')) material_symbol('alternate_email', title: I18n.t('statuses.visibilities.direct'))
end end
end end
def interrelationships_icon(relationships, account_id) def interrelationships_icon(relationships, account_id)
if relationships.following[account_id] && relationships.followed_by[account_id] if relationships.following[account_id] && relationships.followed_by[account_id]
fa_icon('exchange', title: I18n.t('relationships.mutual'), class: 'fa-fw active passive') material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')
elsif relationships.following[account_id] elsif relationships.following[account_id]
fa_icon(locale_direction == 'ltr' ? 'arrow-right' : 'arrow-left', title: I18n.t('relationships.following'), class: 'fa-fw active') material_symbol(locale_direction == 'ltr' ? 'arrow_right_alt' : 'arrow_left_alt', title: I18n.t('relationships.following'), class: 'active')
elsif relationships.followed_by[account_id] elsif relationships.followed_by[account_id]
fa_icon(locale_direction == 'ltr' ? 'arrow-left' : 'arrow-right', title: I18n.t('relationships.followers'), class: 'fa-fw passive') material_symbol(locale_direction == 'ltr' ? 'arrow_left_alt' : 'arrow_right_alt', title: I18n.t('relationships.followers'), class: 'passive')
end end
end end
@ -245,22 +238,6 @@ module ApplicationHelper
full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg')) full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg'))
end end
def instance_presenter
@instance_presenter ||= InstancePresenter.new
end
def favicon_path(size = '48')
instance_presenter.favicon&.file&.url(size)
end
def app_icon_path(size = '48')
instance_presenter.app_icon&.file&.url(size)
end
def use_mask_icon?
instance_presenter.app_icon.blank?
end
# glitch-soc addition to handle the multiple flavors # glitch-soc addition to handle the multiple flavors
def preload_locale_pack def preload_locale_pack
supported_locales = Themes.instance.flavour(current_flavour)['locales'] supported_locales = Themes.instance.flavour(current_flavour)['locales']

View File

@ -13,6 +13,22 @@ module InstanceHelper
safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ') safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ')
end end
def instance_presenter
@instance_presenter ||= InstancePresenter.new
end
def favicon_path(size = '48')
instance_presenter.favicon&.file&.url(size)
end
def app_icon_path(size = '48')
instance_presenter.app_icon&.file&.url(size)
end
def use_mask_icon?
instance_presenter.app_icon.blank?
end
private private
def description_prefix(invite) def description_prefix(invite)

View File

@ -141,7 +141,7 @@ module JsonLdHelper
def safe_for_forwarding?(original, compacted) def safe_for_forwarding?(original, compacted)
original.without('@context', 'signature').all? do |key, value| original.without('@context', 'signature').all? do |key, value|
compacted_value = compacted[key] compacted_value = compacted[key]
return false unless value.class == compacted_value.class return false unless value.instance_of?(compacted_value.class)
if value.is_a?(Hash) if value.is_a?(Hash)
safe_for_forwarding?(value, compacted_value) safe_for_forwarding?(value, compacted_value)

View File

@ -26,11 +26,11 @@ module SettingsHelper
device = session.detection.device device = session.detection.device
if device.mobile? if device.mobile?
'mobile' 'smartphone'
elsif device.tablet? elsif device.tablet?
'tablet' 'tablet'
else else
'desktop' 'desktop_mac'
end end
end end

View File

@ -60,13 +60,13 @@ module StatusesHelper
def fa_visibility_icon(status) def fa_visibility_icon(status)
case status.visibility case status.visibility
when 'public' when 'public'
fa_icon 'globe fw' material_symbol 'globe'
when 'unlisted' when 'unlisted'
fa_icon 'unlock fw' material_symbol 'lock_open'
when 'private' when 'private'
fa_icon 'lock fw' material_symbol 'lock'
when 'direct' when 'direct'
fa_icon 'at fw' material_symbol 'alternate_email'
end end
end end

View File

@ -5,8 +5,10 @@ module ThemeHelper
flavour, theme = flavour_and_skin flavour, theme = flavour_and_skin
if theme == 'system' if theme == 'system'
stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') + ''.html_safe.tap do |tags|
stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') tags << stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
tags << stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
end
else else
stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous' stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous'
end end
@ -16,8 +18,10 @@ module ThemeHelper
_, theme = flavour_and_skin _, theme = flavour_and_skin
if theme == 'system' if theme == 'system'
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') + ''.html_safe.tap do |tags|
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)') tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)')
tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
end
else else
tag.meta name: 'theme-color', content: theme_color_for(theme) tag.meta name: 'theme-color', content: theme_color_for(theme)
end end

View File

@ -2,4 +2,3 @@
that is implicitly generated as the common chunk through webpack's `splitChunks` config */ that is implicitly generated as the common chunk through webpack's `splitChunks` config */
import './public-path'; import './public-path';
import 'font-awesome/css/font-awesome.css';

View File

@ -316,8 +316,8 @@ function loaded() {
const message = const message =
statusEl.dataset.spoiler === 'expanded' statusEl.dataset.spoiler === 'expanded'
? localeData['status.show_less'] ?? 'Show less' ? (localeData['status.show_less'] ?? 'Show less')
: localeData['status.show_more'] ?? 'Show more'; : (localeData['status.show_more'] ?? 'Show more');
spoilerLink.textContent = new IntlMessageFormat( spoilerLink.textContent = new IntlMessageFormat(
message, message,
locale, locale,
@ -431,6 +431,42 @@ Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => {
target.src = target.dataset.static; target.src = target.dataset.static;
}); });
const setInputDisabled = (
input: HTMLInputElement | HTMLSelectElement,
disabled: boolean,
) => {
input.disabled = disabled;
const wrapper = input.closest('.with_label');
if (wrapper) {
wrapper.classList.toggle('disabled', input.disabled);
const hidden =
input.type === 'checkbox' &&
wrapper.querySelector<HTMLInputElement>('input[type=hidden][value="0"]');
if (hidden) {
hidden.disabled = input.disabled;
}
}
};
Rails.delegate(
document,
'#account_statuses_cleanup_policy_enabled',
'change',
({ target }) => {
if (!(target instanceof HTMLInputElement) || !target.form) return;
target.form
.querySelectorAll<
HTMLInputElement | HTMLSelectElement
>('input:not([type=hidden], #account_statuses_cleanup_policy_enabled), select')
.forEach((input) => {
setInputDisabled(input, !target.checked);
});
},
);
// Empty the honeypot fields in JS in case something like an extension // Empty the honeypot fields in JS in case something like an extension
// automatically filled them. // automatically filled them.
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => { Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {

View File

@ -6,5 +6,4 @@ export const submitAccountNote = createDataLoadingThunk(
({ accountId, note }: { accountId: string; note: string }) => ({ accountId, note }: { accountId: string; note: string }) =>
apiSubmitAccountNote(accountId, note), apiSubmitAccountNote(accountId, note),
(relationship) => ({ relationship }), (relationship) => ({ relationship }),
{ skipLoading: true },
); );

View File

@ -1,3 +1,5 @@
import { browserHistory } from 'flavours/glitch/components/router';
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { import {
@ -722,6 +724,16 @@ export const updateAccount = ({ displayName, note, avatar, header, discoverable,
}); });
}; };
export const navigateToProfile = (accountId) => {
return (_dispatch, getState) => {
const acct = getState().accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}`);
}
};
};
export function fetchPinnedAccountsSuggestions(q) { export function fetchPinnedAccountsSuggestions(q) {
return (dispatch) => { return (dispatch) => {
dispatch(fetchPinnedAccountsSuggestionsRequest()); dispatch(fetchPinnedAccountsSuggestionsRequest());

View File

@ -2,6 +2,9 @@ import { createAction } from '@reduxjs/toolkit';
import type { LayoutType } from '../is_mobile'; import type { LayoutType } from '../is_mobile';
export const focusApp = createAction('APP_FOCUS');
export const unfocusApp = createAction('APP_UNFOCUS');
interface ChangeLayoutPayload { interface ChangeLayoutPayload {
layout: LayoutType; layout: LayoutType;
} }

View File

@ -4,6 +4,7 @@ import axios from 'axios';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import api from 'flavours/glitch/api'; import api from 'flavours/glitch/api';
import { browserHistory } from 'flavours/glitch/components/router';
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light'; import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'flavours/glitch/settings'; import { tagHistory } from 'flavours/glitch/settings';
import { recoverHashtags } from 'flavours/glitch/utils/hashtag'; import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
@ -94,9 +95,9 @@ const messages = defineMessages({
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' }, saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
}); });
export const ensureComposeIsVisible = (getState, routerHistory) => { export const ensureComposeIsVisible = (getState) => {
if (!getState().getIn(['compose', 'mounted'])) { if (!getState().getIn(['compose', 'mounted'])) {
routerHistory.push('/publish'); browserHistory.push('/publish');
} }
}; };
@ -117,7 +118,7 @@ export function changeCompose(text) {
}; };
} }
export function replyCompose(status, routerHistory) { export function replyCompose(status) {
return (dispatch, getState) => { return (dispatch, getState) => {
const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']); const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']);
dispatch({ dispatch({
@ -126,7 +127,19 @@ export function replyCompose(status, routerHistory) {
prependCWRe: prependCWRe, prependCWRe: prependCWRe,
}); });
ensureComposeIsVisible(getState, routerHistory); ensureComposeIsVisible(getState);
};
}
export function replyComposeById(statusId) {
return (dispatch, getState) => {
const state = getState();
const status = state.statuses.get(statusId);
if (status) {
const account = state.accounts.get(status.get('account'));
dispatch(replyCompose(status.set('account', account)));
}
}; };
} }
@ -142,38 +155,44 @@ export function resetCompose() {
}; };
} }
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => { export const focusCompose = (defaultText) => (dispatch, getState) => {
dispatch({ dispatch({
type: COMPOSE_FOCUS, type: COMPOSE_FOCUS,
defaultText, defaultText,
}); });
ensureComposeIsVisible(getState, routerHistory); ensureComposeIsVisible(getState);
}; };
export function mentionCompose(account, routerHistory) { export function mentionCompose(account) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: COMPOSE_MENTION, type: COMPOSE_MENTION,
account: account, account: account,
}); });
ensureComposeIsVisible(getState, routerHistory); ensureComposeIsVisible(getState);
}; };
} }
export function directCompose(account, routerHistory) { export function mentionComposeById(accountId) {
return (dispatch, getState) => {
dispatch(mentionCompose(getState().accounts.get(accountId)));
};
}
export function directCompose(account) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch({ dispatch({
type: COMPOSE_DIRECT, type: COMPOSE_DIRECT,
account: account, account: account,
}); });
ensureComposeIsVisible(getState, routerHistory); ensureComposeIsVisible(getState);
}; };
} }
export function submitCompose(routerHistory, overridePrivacy = null) { export function submitCompose(overridePrivacy = null) {
return function (dispatch, getState) { return function (dispatch, getState) {
let status = getState().getIn(['compose', 'text'], ''); let status = getState().getIn(['compose', 'text'], '');
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
@ -230,11 +249,10 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
}, },
}).then(function (response) { }).then(function (response) {
if (routerHistory if ((browserHistory.location.pathname === '/publish' || browserHistory.location.pathname === '/statuses/new')
&& (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new')
&& window.history.state && window.history.state
&& !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) { && !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) {
routerHistory.goBack(); browserHistory.goBack();
} }
dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(insertIntoTagHistory(response.data.tags, status));
@ -272,7 +290,7 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
message: statusId === null ? messages.published : messages.saved, message: statusId === null ? messages.published : messages.saved,
action: messages.open, action: messages.open,
dismissAfter: 10000, dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`), onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`),
})); }));
} }
}).catch(function (error) { }).catch(function (error) {
@ -310,7 +328,7 @@ export function doodleSet(options) {
export function uploadCompose(files) { export function uploadCompose(files) {
return function (dispatch, getState) { return function (dispatch, getState) {
const uploadLimit = 4; const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']); const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0); const progress = new Array(files.length).fill(0);
@ -330,7 +348,7 @@ export function uploadCompose(files) {
dispatch(uploadComposeRequest()); dispatch(uploadComposeRequest());
for (const [i, f] of Array.from(files).entries()) { for (const [i, f] of Array.from(files).entries()) {
if (media.size + i > 3) break; if (media.size + i > (uploadLimit - 1)) break;
resizeImage(f).then(file => { resizeImage(f).then(file => {
const data = new FormData(); const data = new FormData();

View File

@ -1,62 +0,0 @@
import api from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL';
export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL';
export const fetchDirectory = params => (dispatch) => {
dispatch(fetchDirectoryRequest());
api().get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchDirectorySuccess(data));
dispatch(fetchRelationships(data.map(x => x.id)));
}).catch(error => dispatch(fetchDirectoryFail(error)));
};
export const fetchDirectoryRequest = () => ({
type: DIRECTORY_FETCH_REQUEST,
});
export const fetchDirectorySuccess = accounts => ({
type: DIRECTORY_FETCH_SUCCESS,
accounts,
});
export const fetchDirectoryFail = error => ({
type: DIRECTORY_FETCH_FAIL,
error,
});
export const expandDirectory = params => (dispatch, getState) => {
dispatch(expandDirectoryRequest());
const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
api().get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
dispatch(importFetchedAccounts(data));
dispatch(expandDirectorySuccess(data));
dispatch(fetchRelationships(data.map(x => x.id)));
}).catch(error => dispatch(expandDirectoryFail(error)));
};
export const expandDirectoryRequest = () => ({
type: DIRECTORY_EXPAND_REQUEST,
});
export const expandDirectorySuccess = accounts => ({
type: DIRECTORY_EXPAND_SUCCESS,
accounts,
});
export const expandDirectoryFail = error => ({
type: DIRECTORY_EXPAND_FAIL,
error,
});

View File

@ -0,0 +1,37 @@
import type { List as ImmutableList } from 'immutable';
import { apiGetDirectory } from 'flavours/glitch/api/directory';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
export const fetchDirectory = createDataLoadingThunk(
'directory/fetch',
async (params: Parameters<typeof apiGetDirectory>[0]) =>
apiGetDirectory(params),
(data, { dispatch }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchRelationships(data.map((x) => x.id)));
return { accounts: data };
},
);
export const expandDirectory = createDataLoadingThunk(
'directory/expand',
async (params: Parameters<typeof apiGetDirectory>[0], { getState }) => {
const loadedItems = getState().user_lists.getIn([
'directory',
'items',
]) as ImmutableList<unknown>;
return apiGetDirectory({ ...params, offset: loadedItems.size }, 20);
},
(data, { dispatch }) => {
dispatch(importFetchedAccounts(data));
dispatch(fetchRelationships(data.map((x) => x.id)));
return { accounts: data };
},
);

View File

@ -76,8 +76,8 @@ export function importFetchedStatuses(statuses) {
pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id])));
} }
if (status.card?.author_account) { if (status.card) {
pushUnique(accounts, status.card.author_account); status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account));
} }
} }

View File

@ -36,8 +36,15 @@ export function normalizeStatus(status, normalOldStatus, settings) {
normalStatus.poll = status.poll.id; normalStatus.poll = status.poll.id;
} }
if (status.card?.author_account) { if (status.card) {
normalStatus.card = { ...status.card, author_account: status.card.author_account.id }; normalStatus.card = {
...status.card,
authors: status.card.authors.map(author => ({
...author,
accountId: author.account?.id,
account: undefined,
})),
};
} }
if (status.filtered) { if (status.filtered) {

View File

@ -1,7 +1,11 @@
import { boostModal, favouriteModal } from 'flavours/glitch/initial_state';
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts'; import { fetchRelationships } from './accounts';
import { importFetchedAccounts, importFetchedStatus } from './importer'; import { importFetchedAccounts, importFetchedStatus } from './importer';
import { unreblog, reblog } from './interactions_typed';
import { openModal } from './modal';
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST'; export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS'; export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
@ -432,3 +436,61 @@ export function unpinFail(status, error) {
skipLoading: true, skipLoading: true,
}; };
} }
function toggleReblogWithoutConfirmation(status, visibility) {
return (dispatch) => {
if (status.get('reblogged')) {
dispatch(unreblog({ statusId: status.get('id') }));
} else {
dispatch(reblog({ statusId: status.get('id'), visibility }));
}
};
}
export function toggleReblog(statusId, skipModal = false) {
return (dispatch, getState) => {
const state = getState();
let status = state.statuses.get(statusId);
if (!status)
return;
// The reblog modal expects a pre-filled account in status
// TODO: fix this by having the reblog modal get a statusId and do the work itself
status = status.set('account', state.accounts.get(status.get('account')));
const missing_description_setting = state.getIn(['local_settings', 'confirm_boost_missing_media_description']);
const missing_description = status.get('media_attachments').some(item => !item.get('description'));
if (missing_description_setting && missing_description && !status.get('reblogged')) {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)), missingMediaDescription: true } }));
} else if (boostModal && !skipModal) {
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)) } }));
} else {
dispatch(toggleReblogWithoutConfirmation(status));
}
};
}
export function toggleFavourite(statusId, skipModal = false) {
return (dispatch, getState) => {
const state = getState();
let status = state.statuses.get(statusId);
if (!status)
return;
// The favourite modal expects a pre-filled account in status
// TODO: fix this by having the reblog modal get a statusId and do the work itself
status = status.set('account', state.accounts.get(status.get('account')));
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
if (favouriteModal && !skipModal) {
dispatch(openModal({ modalType: 'FAVOURITE', modalProps: { status, onFavourite: (status) => dispatch(favourite(status)) } }));
} else {
dispatch(favourite(status));
}
}
};
}

View File

@ -75,9 +75,17 @@ interface MarkerParam {
} }
function getLastNotificationId(state: RootState): string | undefined { function getLastNotificationId(state: RootState): string | undefined {
// @ts-expect-error state.notifications is not yet typed // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call const enableBeta = state.settings.getIn(
return state.getIn(['notifications', 'lastReadId']); ['notifications', 'groupingBeta'],
false,
) as boolean;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return enableBeta
? state.notificationGroups.lastReadId
: // @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
state.getIn(['notifications', 'lastReadId']);
} }
const buildPostMarkersParams = (state: RootState) => { const buildPostMarkersParams = (state: RootState) => {

View File

@ -0,0 +1,231 @@
import { createAction } from '@reduxjs/toolkit';
import {
apiClearNotifications,
apiFetchNotifications,
} from 'flavours/glitch/api/notifications';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import type {
ApiNotificationGroupJSON,
ApiNotificationJSON,
} from 'flavours/glitch/api_types/notifications';
import { allNotificationTypes } from 'flavours/glitch/api_types/notifications';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
import { usePendingItems } from 'flavours/glitch/initial_state';
import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
import {
selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsShows,
} from 'flavours/glitch/selectors/settings';
import type { AppDispatch } from 'flavours/glitch/store';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from 'flavours/glitch/store/typed_functions';
import { importFetchedAccounts, importFetchedStatuses } from './importer';
import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings';
function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter((item) => item !== filter);
}
function dispatchAssociatedRecords(
dispatch: AppDispatch,
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
) {
const fetchedAccounts: ApiAccountJSON[] = [];
const fetchedStatuses: ApiStatusJSON[] = [];
notifications.forEach((notification) => {
if (notification.type === 'admin.report') {
fetchedAccounts.push(notification.report.target_account);
}
if (notification.type === 'moderation_warning') {
fetchedAccounts.push(notification.moderation_warning.target_account);
}
if ('status' in notification && notification.status) {
fetchedStatuses.push(notification.status);
}
});
if (fetchedAccounts.length > 0)
dispatch(importFetchedAccounts(fetchedAccounts));
if (fetchedStatuses.length > 0)
dispatch(importFetchedStatuses(fetchedStatuses));
}
export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch',
async (_params, { getState }) => {
const activeFilter =
selectSettingsNotificationsQuickFilterActive(getState());
return apiFetchNotifications({
exclude_types:
activeFilter === 'all'
? selectSettingsNotificationsExcludedTypes(getState())
: excludeAllTypesExcept(activeFilter),
});
},
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
notifications;
// TODO: might be worth not using gaps for that…
// if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
if (notifications.length > 1)
payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id });
return payload;
// dispatch(submitMarkers());
},
);
export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }) =>
apiFetchNotifications({ max_id: params.gap.maxId }),
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => {
return apiFetchNotifications({
max_id: undefined,
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
since_id: usePendingItems
? getState().notificationGroups.groups.find(
(group) => group.type !== 'gap',
)?.page_max_id
: undefined,
});
},
({ notifications, accounts, statuses }, { dispatch }) => {
dispatch(importFetchedAccounts(accounts));
dispatch(importFetchedStatuses(statuses));
dispatchAssociatedRecords(dispatch, notifications);
return { notifications };
},
);
export const processNewNotificationForGroups = createAppAsyncThunk(
'notificationGroups/processNew',
(notification: ApiNotificationJSON, { dispatch, getState }) => {
const state = getState();
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
const notificationShows = selectSettingsNotificationsShows(state);
const showInColumn =
activeFilter === 'all'
? notificationShows[notification.type]
: activeFilter === notification.type;
if (!showInColumn) return;
if (
(notification.type === 'mention' || notification.type === 'update') &&
notification.status?.filtered
) {
const filters = notification.status.filtered.filter((result) =>
result.filter.context.includes('notifications'),
);
if (filters.some((result) => result.filter.filter_action === 'hide')) {
return;
}
}
dispatchAssociatedRecords(dispatch, [notification]);
return notification;
},
);
export const loadPending = createAction('notificationGroups/loadPending');
export const updateScrollPosition = createAppAsyncThunk(
'notificationGroups/updateScrollPosition',
({ top }: { top: boolean }, { dispatch, getState }) => {
if (
top &&
getState().notificationGroups.mergedNotifications === 'needs-reload'
) {
void dispatch(fetchNotifications());
}
return { top };
},
);
export const setNotificationsFilter = createAppAsyncThunk(
'notifications/filter/set',
({ filterType }: { filterType: string }, { dispatch }) => {
dispatch({
type: NOTIFICATIONS_FILTER_SET,
path: ['notifications', 'quickFilter', 'active'],
value: filterType,
});
// dispatch(expandNotifications({ forceLoad: true }));
void dispatch(fetchNotifications());
dispatch(saveSettings());
},
);
export const clearNotifications = createDataLoadingThunk(
'notifications/clear',
() => apiClearNotifications(),
);
export const markNotificationsAsRead = createAction(
'notificationGroups/markAsRead',
);
export const mountNotifications = createAppAsyncThunk(
'notificationGroups/mount',
(_, { dispatch, getState }) => {
const state = getState();
if (
state.notificationGroups.mounted === 0 &&
state.notificationGroups.mergedNotifications === 'needs-reload'
) {
void dispatch(fetchNotifications());
}
},
);
export const unmountNotifications = createAction('notificationGroups/unmount');
export const refreshStaleNotificationGroups = createAppAsyncThunk<{
deferredRefresh: boolean;
}>('notificationGroups/refreshStale', (_, { dispatch, getState }) => {
const state = getState();
if (
state.notificationGroups.scrolledToTop ||
!state.notificationGroups.mounted
) {
void dispatch(fetchNotifications());
return { deferredRefresh: false };
}
return { deferredRefresh: true };
});

View File

@ -0,0 +1,22 @@
import { createAction } from '@reduxjs/toolkit';
import {
apiGetNotificationPolicy,
apiUpdateNotificationsPolicy,
} from 'flavours/glitch/api/notification_policies';
import type { NotificationPolicy } from 'flavours/glitch/models/notification_policy';
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
export const fetchNotificationPolicy = createDataLoadingThunk(
'notificationPolicy/fetch',
() => apiGetNotificationPolicy(),
);
export const updateNotificationsPolicy = createDataLoadingThunk(
'notificationPolicy/update',
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
);
export const decreasePendingNotificationsCount = createAction<number>(
'notificationPolicy/decreasePendingNotificationCount',
);

View File

@ -18,6 +18,7 @@ import {
importFetchedStatuses, importFetchedStatuses,
} from './importer'; } from './importer';
import { submitMarkers } from './markers'; import { submitMarkers } from './markers';
import { decreasePendingNotificationsCount } from './notification_policies';
import { notificationsUpdate } from "./notifications_typed"; import { notificationsUpdate } from "./notifications_typed";
import { register as registerPushNotifications } from './push_notifications'; import { register as registerPushNotifications } from './push_notifications';
import { saveSettings } from './settings'; import { saveSettings } from './settings';
@ -43,7 +44,6 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
@ -57,10 +57,6 @@ 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_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST';
export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS';
export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL';
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST'; export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS'; export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL'; export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
@ -81,6 +77,14 @@ export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMIS
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS'; export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL'; export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
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';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST'; export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS'; export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL'; export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
@ -101,6 +105,12 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
} }
}; };
const selectNotificationCountForRequest = (state, id) => {
const requests = state.getIn(['notificationRequests', 'items']);
const thisRequest = requests.find(request => request.get('id') === id);
return thisRequest ? thisRequest.get('notifications_count') : 0;
};
export const loadPending = () => ({ export const loadPending = () => ({
type: NOTIFICATIONS_LOAD_PENDING, type: NOTIFICATIONS_LOAD_PENDING,
}); });
@ -190,8 +200,8 @@ const noOp = () => {};
let expandNotificationsController = new AbortController(); let expandNotificationsController = new AbortController();
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) { export function expandNotifications({ maxId = undefined, forceLoad = false }) {
return (dispatch, getState) => { return async (dispatch, getState) => {
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']); const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
const notifications = getState().get('notifications'); const notifications = getState().get('notifications');
const isLoadingMore = !!maxId; const isLoadingMore = !!maxId;
@ -201,7 +211,6 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
expandNotificationsController.abort(); expandNotificationsController.abort();
expandNotificationsController = new AbortController(); expandNotificationsController = new AbortController();
} else { } else {
done();
return; return;
} }
} }
@ -228,7 +237,8 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
dispatch(expandNotificationsRequest(isLoadingMore)); dispatch(expandNotificationsRequest(isLoadingMore));
api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => { try {
const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal });
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedAccounts(response.data.map(item => item.account)));
@ -238,11 +248,9 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems)); dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
fetchRelatedRelationships(dispatch, response.data); fetchRelatedRelationships(dispatch, response.data);
dispatch(submitMarkers()); dispatch(submitMarkers());
}).catch(error => { } catch(error) {
dispatch(expandNotificationsFail(error, isLoadingMore)); dispatch(expandNotificationsFail(error, isLoadingMore));
}).finally(() => { }
done();
});
}; };
} }
@ -273,16 +281,6 @@ export function expandNotificationsFail(error, isLoadingMore) {
}; };
} }
export function clearNotifications() {
return (dispatch) => {
dispatch({
type: NOTIFICATIONS_CLEAR,
});
api().post('/api/v1/notifications/clear');
};
}
export function scrollTopNotifications(top) { export function scrollTopNotifications(top) {
return { return {
type: NOTIFICATIONS_SCROLL_TOP, type: NOTIFICATIONS_SCROLL_TOP,
@ -434,40 +432,6 @@ export function setBrowserPermission (value) {
}; };
} }
export const fetchNotificationPolicy = () => (dispatch) => {
dispatch(fetchNotificationPolicyRequest());
api().get('/api/v1/notifications/policy').then(({ data }) => {
dispatch(fetchNotificationPolicySuccess(data));
}).catch(err => {
dispatch(fetchNotificationPolicyFail(err));
});
};
export const fetchNotificationPolicyRequest = () => ({
type: NOTIFICATION_POLICY_FETCH_REQUEST,
});
export const fetchNotificationPolicySuccess = policy => ({
type: NOTIFICATION_POLICY_FETCH_SUCCESS,
policy,
});
export const fetchNotificationPolicyFail = error => ({
type: NOTIFICATION_POLICY_FETCH_FAIL,
error,
});
export const updateNotificationsPolicy = params => (dispatch) => {
dispatch(fetchNotificationPolicyRequest());
api().put('/api/v1/notifications/policy', params).then(({ data }) => {
dispatch(fetchNotificationPolicySuccess(data));
}).catch(err => {
dispatch(fetchNotificationPolicyFail(err));
});
};
export const fetchNotificationRequests = () => (dispatch, getState) => { export const fetchNotificationRequests = () => (dispatch, getState) => {
const params = {}; const params = {};
@ -570,11 +534,13 @@ export const fetchNotificationRequestFail = (id, error) => ({
error, error,
}); });
export const acceptNotificationRequest = id => (dispatch) => { export const acceptNotificationRequest = (id) => (dispatch, getState) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(acceptNotificationRequestRequest(id)); dispatch(acceptNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => { api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
dispatch(acceptNotificationRequestSuccess(id)); dispatch(acceptNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => { }).catch(err => {
dispatch(acceptNotificationRequestFail(id, err)); dispatch(acceptNotificationRequestFail(id, err));
}); });
@ -596,11 +562,13 @@ export const acceptNotificationRequestFail = (id, error) => ({
error, error,
}); });
export const dismissNotificationRequest = id => (dispatch) => { export const dismissNotificationRequest = (id) => (dispatch, getState) => {
const count = selectNotificationCountForRequest(getState(), id);
dispatch(dismissNotificationRequestRequest(id)); dispatch(dismissNotificationRequestRequest(id));
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{ api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
dispatch(dismissNotificationRequestSuccess(id)); dispatch(dismissNotificationRequestSuccess(id));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => { }).catch(err => {
dispatch(dismissNotificationRequestFail(id, err)); dispatch(dismissNotificationRequestFail(id, err));
}); });
@ -622,6 +590,62 @@ export const dismissNotificationRequestFail = (id, error) => ({
error, error,
}); });
export const acceptNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/accept`, { id: ids }).then(() => {
dispatch(acceptNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(acceptNotificationRequestFail(ids, err));
});
};
export const acceptNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_REQUEST,
ids,
});
export const acceptNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_ACCEPT_SUCCESS,
ids,
});
export const acceptNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_ACCEPT_FAIL,
ids,
error,
});
export const dismissNotificationRequests = (ids) => (dispatch, getState) => {
const count = ids.reduce((count, id) => count + selectNotificationCountForRequest(getState(), id), 0);
dispatch(acceptNotificationRequestsRequest(ids));
api().post(`/api/v1/notifications/requests/dismiss`, { id: ids }).then(() => {
dispatch(dismissNotificationRequestsSuccess(ids));
dispatch(decreasePendingNotificationsCount(count));
}).catch(err => {
dispatch(dismissNotificationRequestFail(ids, err));
});
};
export const dismissNotificationRequestsRequest = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_REQUEST,
ids,
});
export const dismissNotificationRequestsSuccess = ids => ({
type: NOTIFICATION_REQUESTS_DISMISS_SUCCESS,
ids,
});
export const dismissNotificationRequestsFail = (ids, error) => ({
type: NOTIFICATION_REQUESTS_DISMISS_FAIL,
ids,
error,
});
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => { export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']); const current = getState().getIn(['notificationRequests', 'current']);
const params = { account_id: accountId }; const params = { account_id: accountId };

View File

@ -0,0 +1,18 @@
import { createAppAsyncThunk } from 'flavours/glitch/store';
import { fetchNotifications } from './notification_groups';
import { expandNotifications } from './notifications';
export const initializeNotifications = createAppAsyncThunk(
'notifications/initialize',
(_, { dispatch, getState }) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const enableBeta = getState().settings.getIn(
['notifications', 'groupingBeta'],
false,
) as boolean;
if (enableBeta) void dispatch(fetchNotifications());
else void dispatch(expandNotifications({}));
},
);

View File

@ -1,11 +1,6 @@
import { createAction } from '@reduxjs/toolkit'; import { createAction } from '@reduxjs/toolkit';
import type { ApiAccountJSON } from '../api_types/accounts'; import type { ApiNotificationJSON } from 'flavours/glitch/api_types/notifications';
// To be replaced once ApiNotificationJSON type exists
interface FakeApiNotificationJSON {
type: string;
account: ApiAccountJSON;
}
export const notificationsUpdate = createAction( export const notificationsUpdate = createAction(
'notifications/update', 'notifications/update',
@ -13,7 +8,7 @@ export const notificationsUpdate = createAction(
playSound, playSound,
...args ...args
}: { }: {
notification: FakeApiNotificationJSON; notification: ApiNotificationJSON;
usePendingItems: boolean; usePendingItems: boolean;
playSound: boolean; playSound: boolean;
}) => ({ }) => ({

View File

@ -1,3 +1,5 @@
import { browserHistory } from 'flavours/glitch/components/router';
import api from '../api'; import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose'; import { ensureComposeIsVisible, setComposeToStatus } from './compose';
@ -94,7 +96,7 @@ export function redraft(status, raw_text, content_type) {
}; };
} }
export const editStatus = (id, routerHistory) => (dispatch, getState) => { export const editStatus = (id) => (dispatch, getState) => {
let status = getState().getIn(['statuses', id]); let status = getState().getIn(['statuses', id]);
if (status.get('poll')) { if (status.get('poll')) {
@ -105,7 +107,7 @@ export const editStatus = (id, routerHistory) => (dispatch, getState) => {
api().get(`/api/v1/statuses/${id}/source`).then(response => { api().get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch(fetchStatusSourceSuccess()); dispatch(fetchStatusSourceSuccess());
ensureComposeIsVisible(getState, routerHistory); ensureComposeIsVisible(getState);
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type)); dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
}).catch(error => { }).catch(error => {
dispatch(fetchStatusSourceFail(error)); dispatch(fetchStatusSourceFail(error));
@ -125,7 +127,7 @@ export const fetchStatusSourceFail = error => ({
error, error,
}); });
export function deleteStatus(id, routerHistory, withRedraft = false) { export function deleteStatus(id, withRedraft = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]); let status = getState().getIn(['statuses', id]);
@ -142,7 +144,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
if (withRedraft) { if (withRedraft) {
dispatch(redraft(status, response.data.text, response.data.content_type)); dispatch(redraft(status, response.data.text, response.data.content_type));
ensureComposeIsVisible(getState, routerHistory); ensureComposeIsVisible(getState);
} }
}).catch(error => { }).catch(error => {
dispatch(deleteStatusFail(id, error)); dispatch(deleteStatusFail(id, error));
@ -309,6 +311,21 @@ export function revealStatus(ids) {
}; };
} }
export function toggleStatusSpoilers(statusId) {
return (dispatch, getState) => {
const status = getState().statuses.get(statusId);
if (!status)
return;
if (status.get('hidden')) {
dispatch(revealStatus(statusId));
} else {
dispatch(hideStatus(statusId));
}
};
}
export function toggleStatusCollapse(id, isCollapsed) { export function toggleStatusCollapse(id, isCollapsed) {
return { return {
type: STATUS_COLLAPSE, type: STATUS_COLLAPSE,
@ -349,3 +366,15 @@ export const undoStatusTranslation = (id, pollId) => ({
id, id,
pollId, pollId,
}); });
export const navigateToStatus = (statusId) => {
return (_dispatch, getState) => {
const state = getState();
const accountId = state.statuses.getIn([statusId, 'account']);
const acct = state.accounts.getIn([accountId, 'acct']);
if (acct) {
browserHistory.push(`/@${acct}/${statusId}`);
}
};
};

View File

@ -10,6 +10,7 @@ import {
deleteAnnouncement, deleteAnnouncement,
} from './announcements'; } from './announcements';
import { updateConversations } from './conversations'; import { updateConversations } from './conversations';
import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
import { updateNotifications, expandNotifications } from './notifications'; import { updateNotifications, expandNotifications } from './notifications';
import { updateStatus } from './statuses'; import { updateStatus } from './statuses';
import { import {
@ -36,7 +37,7 @@ const randomUpTo = max =>
* @param {string} channelName * @param {string} channelName
* @param {Object.<string, string>} params * @param {Object.<string, string>} params
* @param {Object} options * @param {Object} options
* @param {function(Function, Function): void} [options.fallback] * @param {function(Function, Function): Promise<void>} [options.fallback]
* @param {function(): void} [options.fillGaps] * @param {function(): void} [options.fillGaps]
* @param {function(object): boolean} [options.accept] * @param {function(object): boolean} [options.accept]
* @returns {function(): void} * @returns {function(): void}
@ -51,14 +52,13 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
let pollingId; let pollingId;
/** /**
* @param {function(Function, Function): void} fallback * @param {function(Function, Function): Promise<void>} fallback
*/ */
const useFallback = fallback => { const useFallback = async fallback => {
fallback(dispatch, () => { await fallback(dispatch, getState);
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
});
}; };
return { return {
@ -77,7 +77,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
}, },
onDisconnect() { onDisconnect() {
dispatch(disconnectTimeline(timelineId)); dispatch(disconnectTimeline({ timeline: timelineId }));
if (options.fallback) { if (options.fallback) {
// @ts-expect-error // @ts-expect-error
@ -98,9 +98,23 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
case 'delete': case 'delete':
dispatch(deleteFromTimelines(data.payload)); dispatch(deleteFromTimelines(data.payload));
break; break;
case 'notification': case 'notification': {
// @ts-expect-error // @ts-expect-error
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); const notificationJSON = JSON.parse(data.payload);
dispatch(updateNotifications(notificationJSON, messages, locale));
// TODO: remove this once the groups feature replaces the previous one
if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
dispatch(processNewNotificationForGroups(notificationJSON));
}
break;
}
case 'notifications_merged':
const state = getState();
if (state.notifications.top || !state.notifications.mounted)
dispatch(expandNotifications({ forceLoad: true, maxId: undefined }));
if(state.settings.getIn(['notifications', 'groupingBeta'], false)) {
dispatch(refreshStaleNotificationGroups());
}
break; break;
case 'conversation': case 'conversation':
// @ts-expect-error // @ts-expect-error
@ -125,21 +139,30 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
/** /**
* @param {Function} dispatch * @param {Function} dispatch
* @param {function(): void} done * @param {Function} getState
*/ */
const refreshHomeTimelineAndNotification = (dispatch, done) => { async function refreshHomeTimelineAndNotification(dispatch, getState) {
// @ts-expect-error await dispatch(expandHomeTimeline({ maxId: undefined }));
dispatch(expandHomeTimeline({}, () =>
// @ts-expect-error // TODO: remove this once the groups feature replaces the previous one
dispatch(expandNotifications({}, () => if(getState().settings.getIn(['notifications', 'groupingBeta'], false)) {
dispatch(fetchAnnouncements(done)))))); // TODO: polling for merged notifications
}; try {
await dispatch(pollRecentGroupNotifications());
} catch (error) {
// TODO
}
} else {
await dispatch(expandNotifications({}));
}
await dispatch(fetchAnnouncements());
}
/** /**
* @returns {function(): void} * @returns {function(): void}
*/ */
export const connectUserStream = () => export const connectUserStream = () =>
// @ts-expect-error
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
/** /**

View File

@ -7,9 +7,11 @@ import { toServerSideType } from 'flavours/glitch/utils/filters';
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses } from './importer';
import { submitMarkers } from './markers'; import { submitMarkers } from './markers';
import {timelineDelete} from './timelines_typed';
export { disconnectTimeline } from './timelines_typed';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
@ -18,7 +20,6 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
@ -73,16 +74,10 @@ export function updateTimeline(timeline, status, accept) {
export function deleteFromTimelines(id) { export function deleteFromTimelines(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
const accountId = getState().getIn(['statuses', id, 'account']); const accountId = getState().getIn(['statuses', id, 'account']);
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')).valueSeq().toJSON();
const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
dispatch({ dispatch(timelineDelete({ statusId: id, accountId, references, reblogOf }));
type: TIMELINE_DELETE,
id,
accountId,
references,
reblogOf,
});
}; };
} }
@ -92,21 +87,18 @@ export function clearTimeline(timeline) {
}; };
} }
const noOp = () => {};
const parseTags = (tags = {}, mode) => { const parseTags = (tags = {}, mode) => {
return (tags[mode] || []).map((tag) => { return (tags[mode] || []).map((tag) => {
return tag.value; return tag.value;
}); });
}; };
export function expandTimeline(timelineId, path, params = {}, done = noOp) { export function expandTimeline(timelineId, path, params = {}) {
return (dispatch, getState) => { return async (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
const isLoadingMore = !!params.max_id; const isLoadingMore = !!params.max_id;
if (timeline.get('isLoading')) { if (timeline.get('isLoading')) {
done();
return; return;
} }
@ -125,7 +117,8 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
dispatch(expandTimelineRequest(timelineId, isLoadingMore)); dispatch(expandTimelineRequest(timelineId, isLoadingMore));
api().get(path, { params }).then(response => { try {
const response = await api().get(path, { params });
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data)); dispatch(importFetchedStatuses(response.data));
@ -143,52 +136,49 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
if (timelineId === 'home') { if (timelineId === 'home') {
dispatch(submitMarkers()); dispatch(submitMarkers());
} }
}).catch(error => { } catch(error) {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
}).finally(() => { }
done();
});
}; };
} }
export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) { export function fillTimelineGaps(timelineId, path, params = {}) {
return (dispatch, getState) => { return async (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
const items = timeline.get('items'); const items = timeline.get('items');
const nullIndexes = items.map((statusId, index) => statusId === null ? index : null); const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null); const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
// Only expand at most two gaps to avoid doing too many requests // Only expand at most two gaps to avoid doing too many requests
done = gaps.take(2).reduce((done, maxId) => { for (const maxId of gaps.take(2)) {
return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done))); await dispatch(expandTimeline(timelineId, path, { ...params, maxId }));
}, done); }
done();
}; };
} }
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia });
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia });
export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); export const expandDirectTimeline = ({ maxId } = {}) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId });
export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId }); export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { export const expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId });
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => {
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
max_id: maxId, max_id: maxId,
any: parseTags(tags, 'any'), any: parseTags(tags, 'any'),
all: parseTags(tags, 'all'), all: parseTags(tags, 'all'),
none: parseTags(tags, 'none'), none: parseTags(tags, 'none'),
local: local, local: local,
}, done); });
}; };
export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done); export const fillHomeTimelineGaps = () => fillTimelineGaps('home', '/api/v1/timelines/home', {});
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly }, done); export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) => fillTimelineGaps(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia, allow_local_only: !!allowLocalOnly });
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done); export const fillCommunityTimelineGaps = ({ onlyMedia } = {}) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia });
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done); export const fillListTimelineGaps = (id) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {});
export function expandTimelineRequest(timeline, isLoadingMore) { export function expandTimelineRequest(timeline, isLoadingMore) {
return { return {
@ -237,12 +227,6 @@ export function connectTimeline(timeline) {
}; };
} }
export const disconnectTimeline = timeline => ({
type: TIMELINE_DISCONNECT,
timeline,
usePendingItems: preferPendingItems,
});
export const markAsPartial = timeline => ({ export const markAsPartial = timeline => ({
type: TIMELINE_MARK_AS_PARTIAL, type: TIMELINE_MARK_AS_PARTIAL,
timeline, timeline,

View File

@ -0,0 +1,20 @@
import { createAction } from '@reduxjs/toolkit';
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
export const disconnectTimeline = createAction(
'timeline/disconnect',
({ timeline }: { timeline: string }) => ({
payload: {
timeline,
usePendingItems: preferPendingItems,
},
}),
);
export const timelineDelete = createAction<{
statusId: string;
accountId: string;
references: string[];
reblogOf: string | null;
}>('timelines/delete');

View File

@ -51,7 +51,7 @@ export const fetchTrendingLinks = () => (dispatch) => {
api() api()
.get('/api/v1/trends/links', { params: { limit: 20 } }) .get('/api/v1/trends/links', { params: { limit: 20 } })
.then(({ data }) => { .then(({ data }) => {
dispatch(importFetchedAccounts(data.map(link => link.author_account).filter(account => !!account))); dispatch(importFetchedAccounts(data.flatMap(link => link.authors.map(author => author.account)).filter(account => !!account)));
dispatch(fetchTrendingLinksSuccess(data)); dispatch(fetchTrendingLinksSuccess(data));
}) })
.catch(err => dispatch(fetchTrendingLinksFail(err))); .catch(err => dispatch(fetchTrendingLinksFail(err)));

View File

@ -59,16 +59,49 @@ export default function api(withAuthorization = true) {
}); });
} }
type RequestParamsOrData = Record<string, unknown>;
export async function apiRequest<ApiResponse = unknown>( export async function apiRequest<ApiResponse = unknown>(
method: Method, method: Method,
url: string, url: string,
params?: Record<string, unknown>, args: {
params?: RequestParamsOrData;
data?: RequestParamsOrData;
} = {},
) { ) {
const { data } = await api().request<ApiResponse>({ const { data } = await api().request<ApiResponse>({
method, method,
url: '/api/' + url, url: '/api/' + url,
data: params, ...args,
}); });
return data; return data;
} }
export async function apiRequestGet<ApiResponse = unknown>(
url: string,
params?: RequestParamsOrData,
) {
return apiRequest<ApiResponse>('GET', url, { params });
}
export async function apiRequestPost<ApiResponse = unknown>(
url: string,
data?: RequestParamsOrData,
) {
return apiRequest<ApiResponse>('POST', url, { data });
}
export async function apiRequestPut<ApiResponse = unknown>(
url: string,
data?: RequestParamsOrData,
) {
return apiRequest<ApiResponse>('PUT', url, { data });
}
export async function apiRequestDelete<ApiResponse = unknown>(
url: string,
params?: RequestParamsOrData,
) {
return apiRequest<ApiResponse>('DELETE', url, { params });
}

View File

@ -1,7 +1,7 @@
import { apiRequest } from 'flavours/glitch/api'; import { apiRequestPost } from 'flavours/glitch/api';
import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships'; import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships';
export const apiSubmitAccountNote = (id: string, value: string) => export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequest<ApiRelationshipJSON>('post', `v1/accounts/${id}/note`, { apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
comment: value, comment: value,
}); });

View File

@ -0,0 +1,15 @@
import { apiRequestGet } from 'flavours/glitch/api';
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
export const apiGetDirectory = (
params: {
order: string;
local: boolean;
offset?: number;
},
limit = 20,
) =>
apiRequestGet<ApiAccountJSON[]>('v1/directory', {
...params,
limit,
});

View File

@ -1,10 +1,10 @@
import { apiRequest } from 'flavours/glitch/api'; import { apiRequestPost } from 'flavours/glitch/api';
import type { Status, StatusVisibility } from 'flavours/glitch/models/status'; import type { Status, StatusVisibility } from 'flavours/glitch/models/status';
export const apiReblog = (statusId: string, visibility: StatusVisibility) => export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
apiRequest<{ reblog: Status }>('post', `v1/statuses/${statusId}/reblog`, { apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
visibility, visibility,
}); });
export const apiUnreblog = (statusId: string) => export const apiUnreblog = (statusId: string) =>
apiRequest<Status>('post', `v1/statuses/${statusId}/unreblog`); apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);

View File

@ -0,0 +1,9 @@
import { apiRequestGet, apiRequestPut } from 'flavours/glitch/api';
import type { NotificationPolicyJSON } from 'flavours/glitch/api_types/notification_policies';
export const apiGetNotificationPolicy = () =>
apiRequestGet<NotificationPolicyJSON>('v2/notifications/policy');
export const apiUpdateNotificationsPolicy = (
policy: Partial<NotificationPolicyJSON>,
) => apiRequestPut<NotificationPolicyJSON>('v2/notifications/policy', policy);

View File

@ -0,0 +1,26 @@
import api, { apiRequest, getLinks } from 'flavours/glitch/api';
import type { ApiNotificationGroupsResultJSON } from 'flavours/glitch/api_types/notifications';
export const apiFetchNotifications = async (params?: {
exclude_types?: string[];
max_id?: string;
since_id?: string;
}) => {
const response = await api().request<ApiNotificationGroupsResultJSON>({
method: 'GET',
url: '/api/v2_alpha/notifications',
params,
});
const { statuses, accounts, notification_groups } = response.data;
return {
statuses,
accounts,
notifications: notification_groups,
links: getLinks(response),
};
};
export const apiClearNotifications = () =>
apiRequest<undefined>('POST', 'v1/notifications/clear');

View File

@ -0,0 +1,15 @@
// See app/serializers/rest/notification_policy_serializer.rb
export type NotificationPolicyValue = 'accept' | 'filter' | 'drop';
export interface NotificationPolicyJSON {
for_not_following: NotificationPolicyValue;
for_not_followers: NotificationPolicyValue;
for_new_accounts: NotificationPolicyValue;
for_private_mentions: NotificationPolicyValue;
for_limited_accounts: NotificationPolicyValue;
summary: {
pending_requests_count: number;
pending_notifications_count: number;
};
}

View File

@ -0,0 +1,151 @@
// See app/serializers/rest/notification_group_serializer.rb
import type { AccountWarningAction } from 'flavours/glitch/models/notification_group';
import type { ApiAccountJSON } from './accounts';
import type { ApiReportJSON } from './reports';
import type { ApiStatusJSON } from './statuses';
// See app/model/notification.rb
export const allNotificationTypes = [
'follow',
'follow_request',
'favourite',
'reblog',
'mention',
'poll',
'status',
'update',
'admin.sign_up',
'admin.report',
'moderation_warning',
'severed_relationships',
];
export type NotificationWithStatusType =
| 'favourite'
| 'reblog'
| 'status'
| 'mention'
| 'poll'
| 'update';
export type NotificationType =
| NotificationWithStatusType
| 'follow'
| 'follow_request'
| 'moderation_warning'
| 'severed_relationships'
| 'admin.sign_up'
| 'admin.report';
export interface BaseNotificationJSON {
id: string;
type: NotificationType;
created_at: string;
group_key: string;
account: ApiAccountJSON;
}
export interface BaseNotificationGroupJSON {
group_key: string;
notifications_count: number;
type: NotificationType;
sample_account_ids: string[];
latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly
most_recent_notification_id: string;
page_min_id?: string;
page_max_id?: string;
}
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
type: NotificationWithStatusType;
status_id: string | null;
}
interface NotificationWithStatusJSON extends BaseNotificationJSON {
type: NotificationWithStatusType;
status: ApiStatusJSON | null;
}
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
type: 'admin.report';
report: ApiReportJSON;
}
interface ReportNotificationJSON extends BaseNotificationJSON {
type: 'admin.report';
report: ApiReportJSON;
}
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
type: SimpleNotificationTypes;
}
interface SimpleNotificationJSON extends BaseNotificationJSON {
type: SimpleNotificationTypes;
}
export interface ApiAccountWarningJSON {
id: string;
action: AccountWarningAction;
text: string;
status_ids: string[];
created_at: string;
target_account: ApiAccountJSON;
appeal: unknown;
}
interface ModerationWarningNotificationGroupJSON
extends BaseNotificationGroupJSON {
type: 'moderation_warning';
moderation_warning: ApiAccountWarningJSON;
}
interface ModerationWarningNotificationJSON extends BaseNotificationJSON {
type: 'moderation_warning';
moderation_warning: ApiAccountWarningJSON;
}
export interface ApiAccountRelationshipSeveranceEventJSON {
id: string;
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
purged: boolean;
target_name: string;
followers_count: number;
following_count: number;
created_at: string;
}
interface AccountRelationshipSeveranceNotificationGroupJSON
extends BaseNotificationGroupJSON {
type: 'severed_relationships';
event: ApiAccountRelationshipSeveranceEventJSON;
}
interface AccountRelationshipSeveranceNotificationJSON
extends BaseNotificationJSON {
type: 'severed_relationships';
event: ApiAccountRelationshipSeveranceEventJSON;
}
export type ApiNotificationJSON =
| SimpleNotificationJSON
| ReportNotificationJSON
| AccountRelationshipSeveranceNotificationJSON
| NotificationWithStatusJSON
| ModerationWarningNotificationJSON;
export type ApiNotificationGroupJSON =
| SimpleNotificationGroupJSON
| ReportNotificationGroupJSON
| AccountRelationshipSeveranceNotificationGroupJSON
| NotificationGroupWithStatusJSON
| ModerationWarningNotificationGroupJSON;
export interface ApiNotificationGroupsResultJSON {
accounts: ApiAccountJSON[];
statuses: ApiStatusJSON[];
notification_groups: ApiNotificationGroupJSON[];
}

View File

@ -0,0 +1,16 @@
import type { ApiAccountJSON } from './accounts';
export type ReportCategory = 'other' | 'spam' | 'legal' | 'violation';
export interface ApiReportJSON {
id: string;
action_taken: unknown;
action_taken_at: unknown;
category: ReportCategory;
comment: string;
forwarded: boolean;
created_at: string;
status_ids: string[];
rule_ids: string[];
target_account: ApiAccountJSON;
}

View File

@ -30,6 +30,12 @@ export interface ApiMentionJSON {
acct: string; acct: string;
} }
export interface ApiPreviewCardAuthorJSON {
name: string;
url: string;
account?: ApiAccountJSON;
}
export interface ApiPreviewCardJSON { export interface ApiPreviewCardJSON {
url: string; url: string;
title: string; title: string;
@ -38,6 +44,7 @@ export interface ApiPreviewCardJSON {
type: string; type: string;
author_name: string; author_name: string;
author_url: string; author_url: string;
author_account?: ApiAccountJSON;
provider_name: string; provider_name: string;
provider_url: string; provider_url: string;
html: string; html: string;
@ -48,6 +55,30 @@ export interface ApiPreviewCardJSON {
embed_url: string; embed_url: string;
blurhash: string; blurhash: string;
published_at: string; published_at: string;
authors: ApiPreviewCardAuthorJSON[];
}
export type FilterContext =
| 'home'
| 'notifications'
| 'public'
| 'thread'
| 'account';
export interface ApiFilterJSON {
id: string;
title: string;
context: FilterContext;
expires_at: string;
filter_action: 'warn' | 'hide';
keywords?: unknown[]; // TODO: FilterKeywordSerializer
statuses?: unknown[]; // TODO: FilterStatusSerializer
}
export interface ApiFilterResultJSON {
filter: ApiFilterJSON;
keyword_matches: string[];
status_matches: string[];
} }
export interface ApiStatusJSON { export interface ApiStatusJSON {
@ -72,8 +103,7 @@ export interface ApiStatusJSON {
bookmarked?: boolean; bookmarked?: boolean;
pinned?: boolean; pinned?: boolean;
// filtered: FilterResult[] filtered?: ApiFilterResultJSON[];
filtered: unknown; // TODO
content?: string; content?: string;
text?: string; text?: string;

View File

@ -1,5 +1,4 @@
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';
import 'font-awesome/css/font-awesome.css';
export function start() { export function start() {
require.context('@/images/', true, /\.(jpg|png|svg)$/); require.context('@/images/', true, /\.(jpg|png|svg)$/);

View File

@ -106,7 +106,7 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica
</> </>
); );
} else if (defaultAction === 'mute') { } else if (defaultAction === 'mute') {
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={handleMute} />; buttons = <Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />;
} else if (defaultAction === 'block') { } else if (defaultAction === 'block') {
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />; buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
} else if (!account.get('suspended') && !account.get('moved') || following) { } else if (!account.get('suspended') && !account.get('moved') || following) {
@ -131,7 +131,7 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica
return ( return (
<div className={classNames('account', { 'account--minimal': minimal })}> <div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'> <div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`}> <Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={account.get('url')} to={`/@${account.get('acct')}`} data-hover-card-account={account.get('id')}>
<div className='account__avatar-wrapper'> <div className='account__avatar-wrapper'>
<Avatar account={account} size={size} /> <Avatar account={account} size={size} />
</div> </div>

Some files were not shown because too many files have changed in this diff Show More