Compare commits

..

55 Commits

Author SHA1 Message Date
Terence Eden b923a4c755
Prevent split line between icon and number on reposts & favourites (#26004) 2023-07-16 10:06:33 +02:00
Claire 71db616fed
Change “About” and “Privacy policy” links to open in a new tab in advanced interface (#25973) 2023-07-13 17:59:15 +02:00
Stanislas Signoud 5fad7bd58a
Change links in multi-column mode so tabs are open in single-column mode (#25893) 2023-07-13 17:18:09 +02:00
Claire 41f65edb21
Fix embed dropdown menu item for unauthenticated users (#25964) 2023-07-13 15:53:03 +02:00
Matt Jankowski 644c5fddd8
Refactor `Status.tagged_with_all` for brakeman SQL injection warning (#25941) 2023-07-13 15:52:37 +02:00
Renaud Chaput 70cc7bdbba
Remove some recently-updated packages from Renovabot ignore config (#25960) 2023-07-13 13:34:38 +02:00
Claire 5a3f174d56
Fix follow link style in embeds (#25965) 2023-07-13 12:58:56 +02:00
renovate[bot] ba0649f042
Update dependency postcss to v8.4.25 (#25961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-13 12:20:03 +02:00
renovate[bot] a4e6ff0d53
Update dependency react-textarea-autosize to v8.5.2 (#25962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-13 12:19:54 +02:00
Renaud Chaput a7253075d1
Upgrade to `typescript-eslint` v6 (#25904) 2023-07-13 11:49:16 +02:00
renovate[bot] 3ed9b55cb3
Update dependency rubocop-rails to v2.20.1 (#25493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2023-07-13 11:44:02 +02:00
Renaud Chaput a75138d073
Convert Home timeline components to Typescript (#25583) 2023-07-13 11:28:55 +02:00
Renaud Chaput 73b64b8917
Upgrade to Prettier 3 (#25902) 2023-07-13 11:26:45 +02:00
renovate[bot] 0d7340380c
Update dependency glob to v10.3.3 (#25959)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-13 11:20:20 +02:00
renovate[bot] 6be9f95a22
Update dependency core-js to v3.31.1 (#25958)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-13 11:17:46 +02:00
Michael Stanclift 063482a63f
Fix trending publishers table not rendering correctly on narrow screens (#25945) 2023-07-13 11:12:51 +02:00
Nick Schonning 1a6c2e450a
Update rubocop to v1.54.1 (#25627) 2023-07-13 11:11:55 +02:00
renovate[bot] e7b0d1e23c
Update dependency chewy to v7.3.3 (#25940)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-13 11:07:26 +02:00
Matt Jankowski ce43ed144c
Rails 7.0 update (#25668) 2023-07-13 09:36:07 +02:00
Eugen Rochko 8d0c69529a
Change markers API to use a replica (#25851) 2023-07-12 18:57:40 +02:00
Eugen Rochko fdc3ff7c2d
Change notifications API to use a replica (#25874) 2023-07-12 17:06:00 +02:00
renovate[bot] 82e477b184
Update dependency capistrano-rails to v1.6.3 (#25934)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 14:19:51 +02:00
Matt Jankowski 1ef014802b
Refactor `Trends::Query` to avoid brakeman sql injection warnings (#25881) 2023-07-12 14:19:20 +02:00
Renaud Chaput ecd8e0d612
Update Stylelint (#25819) 2023-07-12 12:31:23 +02:00
Renaud Chaput be34b437ed
Update `haml-lint` (#25929) 2023-07-12 12:31:10 +02:00
Matt Jankowski f831452037
Refactor `Snowflake` to avoid brakeman sql injection warnings (#25879) 2023-07-12 10:44:58 +02:00
Matt Jankowski 6c5a2233a8
Fix `RSpec/StubbedMock` cop (#25552)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-07-12 10:20:10 +02:00
Matt Jankowski 2e1391fdd2
Fix `Naming/MemoizedInstanceVariableName` cop (#25928) 2023-07-12 10:08:51 +02:00
Matt Jankowski 5134fc65e2
Fix `Naming/AccessorMethodName` cop (#25924) 2023-07-12 10:03:19 +02:00
Matt Jankowski b8b2470cf8
Fix `Style/SlicingWithRange` cop (#25923) 2023-07-12 10:03:06 +02:00
Matt Jankowski 658742b3cd
Fix `Lint/AmbiguousBlockAssociation` cop (#25921) 2023-07-12 10:02:41 +02:00
Matt Jankowski b786911c55
Fix `Lint/SendWithMixinArgument` cop (#25920) 2023-07-12 10:02:32 +02:00
Matt Jankowski 74806deb2c
Fix `RSpec/SubjectStub` cop (#25550)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2023-07-12 10:02:19 +02:00
Matt Jankowski 7824df0eca
Exclude `lib/linter` from simplecov report (#25916) 2023-07-12 09:51:59 +02:00
Matt Jankowski c75df62ccc
Fix `RSpec/SubjectDeclaration` cop (#25312) 2023-07-12 09:49:33 +02:00
Nick Schonning f134a5f9d8
Run Rubocop on Rakefile (#23871) 2023-07-12 09:47:54 +02:00
Nick Schonning 1d557305d2
Enable Rubocop Style/FrozenStringLiteralComment (#23793) 2023-07-12 09:47:08 +02:00
Nick Schonning 9e8bc56d5a
Enable Rubocop Style/Semicolon with config (#23652) 2023-07-12 09:44:15 +02:00
renovate[bot] 8e0fd2d619
Update babel monorepo (#25930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-12 09:05:17 +02:00
Stanislas Signoud 1392f31ed8
Fix sounds not being loaded from assets host (#25931) 2023-07-12 03:02:32 +02:00
Stanislas Signoud ca955ada0b
Use invariant colors on notification toasts (#25919) 2023-07-11 23:30:21 +02:00
Claire 3b92499cbc
Fix incorrect syntax in Github action configuration (#25918) 2023-07-11 19:52:37 +02:00
Matt Jankowski a02ae37766
Run the rebase conflict checker once an hour (#25914) 2023-07-11 19:40:51 +02:00
Claire 9411fa4d36
Update brakeman ignores (#25912) 2023-07-11 17:08:37 +02:00
Nick Schonning e11032585b
Run brakeman in GitHub Actions (#23713) 2023-07-11 15:23:57 +02:00
trwnh 3aa153694e
Fix changelog referencing wrong API version (#25857) 2023-07-11 14:53:58 +02:00
Renaud Chaput 518890a9f1
Fixes `latest` Docker tag (#25812) 2023-07-11 14:52:00 +02:00
Trevor Wolf ea10febd25
fix buttons showing inconsistent styles (#25903) 2023-07-11 12:26:09 +02:00
jsgoldstein 99be47f8b9
Change searching with # to include account index (#25638) 2023-07-10 20:58:13 +02:00
Claire af54bf52c8
Fix filters not applying to explore tab (#25887) 2023-07-10 19:33:07 +02:00
Claire 999c343946
Fix remote accounts being possibly persisted to database with incomplete protocol values (#25886) 2023-07-10 18:42:19 +02:00
Claire 4b5851974c
Fix moderation interface for remote instances with a .zip TLD (#25885) 2023-07-10 18:42:10 +02:00
Claire c27b82a437
Add `forward_to_domains` parameter to `POST /api/v1/reports` (#25866) 2023-07-10 18:26:56 +02:00
Matt Jankowski f3fca78756
Refactor `NotificationMailer` to use parameterization (#25718) 2023-07-10 03:06:22 +02:00
Eugen Rochko a1f5188c8c
Change feed merge, unmerge and regeneration workers to use a replica (#25849) 2023-07-10 03:06:09 +02:00
572 changed files with 2609 additions and 1829 deletions

View File

@ -325,8 +325,8 @@ module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict-type-checked',
'plugin:@typescript-eslint/stylistic-type-checked',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
@ -338,7 +338,7 @@ module.exports = {
],
parserOptions: {
project: './tsconfig.json',
project: true,
tsconfigRootDir: __dirname,
},
@ -348,6 +348,7 @@ module.exports = {
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
"@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}],
'jsdoc/require-jsdoc': 'off',

View File

@ -15,7 +15,6 @@
// Ignore major version bumps for these node packages
matchManagers: ['npm'],
matchPackageNames: [
'@rails/ujs', // Needs to match the major Rails version
'tesseract.js', // Requires code changes
'react-hotkeys', // Requires code changes
@ -51,12 +50,6 @@
'sidekiq', // Requires manual upgrade
'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
'redis', // Requires manual upgrade and sync with Sidekiq version
'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964
// Needs major Rails version bump
'rack',
'rails',
'rails-i18n',
],
matchUpdateTypes: ['major'],
enabled: false,

View File

@ -25,25 +25,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: hadolint/hadolint-action@v3.1.0
# The same driver needs to be used for every node, so we want a local TCP builder for the AMD64 build
- name: Start a local Docker Builder
run: |
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
with:
driver: remote
endpoint: tcp://localhost:1234
platforms: linux/amd64
append: |
- endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865
platforms: linux/arm64
name: mastodon-docker-builder-arm64-01
driver-opts:
- servername=mastodon-docker-builder-arm64-01
env:
BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }}
BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }}
BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }}
- name: Log in to Docker Hub
uses: docker/login-action@v2
@ -69,7 +52,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') && 'auto' || 'false' }}
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
tags: |
type=edge,branch=main
type=pep440,pattern={{raw}}

View File

@ -18,25 +18,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: hadolint/hadolint-action@v3.1.0
# The same driver needs to be used for every node, so we want a local TCP builder for the AMD64 build
- name: Start a local Docker Builder
run: |
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
with:
driver: remote
endpoint: tcp://localhost:1234
platforms: linux/amd64
append: |
- endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865
platforms: linux/arm64
name: mastodon-docker-builder-arm64-01
driver-opts:
- servername=mastodon-docker-builder-arm64-01
env:
BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }}
BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }}
BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }}
- name: Log in to the Github Container registry
uses: docker/login-action@v2

40
.github/workflows/bundler-audit.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Bundler Audit
on:
push:
branches-ignore:
- 'dependabot/**'
paths:
- 'Gemfile*'
- '.ruby-version'
- '.bundler-audit.yml'
- '.github/workflows/bundler-audit.yml'
pull_request:
paths:
- 'Gemfile*'
- '.ruby-version'
- '.bundler-audit.yml'
- '.github/workflows/bundler-audit.yml'
schedule:
- cron: '0 5 * * 1'
jobs:
security:
runs-on: ubuntu-latest
steps:
- name: Clone repository
uses: actions/checkout@v3
- name: Install native Ruby dependencies
run: sudo apt-get install -y libicu-dev libidn11-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Run bundler-audit
run: bundle exec bundler-audit

View File

@ -8,7 +8,7 @@ on:
- 'Gemfile*'
- '.rubocop*.yml'
- '.ruby-version'
- '.bundler-audit.yml'
- 'config/brakeman.ignore'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
@ -18,7 +18,7 @@ on:
- 'Gemfile*'
- '.rubocop*.yml'
- '.ruby-version'
- '.bundler-audit.yml'
- 'config/brakeman.ignore'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
@ -46,5 +46,6 @@ jobs:
- name: Run rubocop
run: bundle exec rubocop
- name: Run bundler-audit
run: bundle exec bundler-audit
- name: Run brakeman
if: always() # Run both checks, even if the first failed
run: bundle exec brakeman

View File

@ -1,17 +1,8 @@
name: PR Needs Rebase
on:
push:
branches-ignore:
- 'dependabot/**'
- 'renovate/**'
- 'l10n_main'
pull_request_target:
branches-ignore:
- 'dependabot/**'
- 'renovate/**'
- 'l10n_main'
types: [synchronize]
schedule:
- cron: '0 * * * *'
permissions:
pull-requests: write

View File

@ -1,73 +1,23 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
# on 2023-03-15 00:55:01 -0400 using Haml-Lint version 0.45.0.
# on 2023-07-11 23:58:05 +0200 using Haml-Lint version 0.48.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
# Offense count: 63
# Offense count: 94
RuboCop:
exclude:
- 'app/views/accounts/_og.html.haml'
- 'app/views/admin/account_warnings/_account_warning.html.haml'
- 'app/views/admin/accounts/index.html.haml'
- 'app/views/admin/accounts/show.html.haml'
- 'app/views/admin/announcements/edit.html.haml'
- 'app/views/admin/announcements/new.html.haml'
- 'app/views/admin/disputes/appeals/_appeal.html.haml'
- 'app/views/admin/domain_blocks/edit.html.haml'
- 'app/views/admin/domain_blocks/new.html.haml'
- 'app/views/admin/ip_blocks/new.html.haml'
- 'app/views/admin/reports/actions/preview.html.haml'
- 'app/views/admin/reports/index.html.haml'
- 'app/views/admin/reports/show.html.haml'
- 'app/views/admin/roles/_form.html.haml'
- 'app/views/admin/settings/about/show.html.haml'
- 'app/views/admin/settings/appearance/show.html.haml'
- 'app/views/admin/settings/registrations/show.html.haml'
- 'app/views/admin/statuses/show.html.haml'
- 'app/views/auth/registrations/new.html.haml'
- 'app/views/disputes/strikes/show.html.haml'
- 'app/views/filters/_filter_fields.html.haml'
- 'app/views/invites/_form.html.haml'
- 'app/views/layouts/application.html.haml'
- 'app/views/layouts/error.html.haml'
- 'app/views/notification_mailer/_status.html.haml'
- 'app/views/settings/applications/_fields.html.haml'
- 'app/views/settings/imports/show.html.haml'
- 'app/views/settings/preferences/appearance/show.html.haml'
- 'app/views/settings/preferences/other/show.html.haml'
- 'app/views/statuses/_detailed_status.html.haml'
- 'app/views/statuses/_poll.html.haml'
- 'app/views/statuses/show.html.haml'
- 'app/views/statuses_cleanup/show.html.haml'
- 'app/views/user_mailer/warning.html.haml'
enabled: false
# Offense count: 913
# Offense count: 960
LineLength:
enabled: false
# Offense count: 22
UnnecessaryStringOutput:
exclude:
- 'app/views/accounts/show.html.haml'
- 'app/views/admin/custom_emojis/_custom_emoji.html.haml'
- 'app/views/admin/relays/_relay.html.haml'
- 'app/views/admin/rules/_rule.html.haml'
- 'app/views/admin/statuses/index.html.haml'
- 'app/views/auth/registrations/_sessions.html.haml'
- 'app/views/disputes/strikes/show.html.haml'
- 'app/views/notification_mailer/_status.html.haml'
- 'app/views/settings/two_factor_authentication_methods/index.html.haml'
- 'app/views/statuses/_detailed_status.html.haml'
- 'app/views/statuses/_poll.html.haml'
- 'app/views/statuses/_simple_status.html.haml'
- 'app/views/user_mailer/suspicious_sign_in.html.haml'
- 'app/views/user_mailer/webauthn_credential_added.html.haml'
- 'app/views/user_mailer/webauthn_credential_deleted.html.haml'
- 'app/views/user_mailer/welcome.html.haml'
enabled: false
# Offense count: 3
ViewLength:

View File

@ -24,7 +24,6 @@ AllCops:
Exclude:
- db/schema.rb
- 'bin/*'
- 'Rakefile'
- 'node_modules/**/*'
- 'Vagrantfile'
- 'vendor/**/*'
@ -192,6 +191,11 @@ Style/RedundantBegin:
Style/RescueStandardError:
EnforcedStyle: implicit
# Reason: Simplify some spec layouts
# https://docs.rubocop.org/rubocop/cops_style.html#stylesemicolon
Style/Semicolon:
AllowAsExpressionSeparator: true
# Reason: Originally disabled for CodeClimate, and no config consensus has been found
# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray
Style/SymbolArray:

View File

@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.52.1.
# using RuboCop version 1.54.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@ -28,7 +28,6 @@ Layout/ArgumentAlignment:
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
Layout/HashAlignment:
Exclude:
- 'config/boot.rb'
- 'config/environments/production.rb'
- 'config/initializers/rack_attack.rb'
- 'config/routes.rb'
@ -48,15 +47,6 @@ Layout/SpaceInLambdaLiteral:
- 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedMethods, AllowedPatterns.
Lint/AmbiguousBlockAssociation:
Exclude:
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb'
- 'spec/services/activitypub/process_status_update_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
# Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock:
Exclude:
@ -106,11 +96,6 @@ Lint/OrAssignmentToConstant:
Exclude:
- 'lib/sanitize_ext/sanitize_config.rb'
# This cop supports safe autocorrection (--autocorrect).
Lint/SendWithMixinArgument:
Exclude:
- 'config/application.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
@ -165,10 +150,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity:
Max: 27
Naming/AccessorMethodName:
Exclude:
- 'app/controllers/auth/sessions_controller.rb'
# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms.
# CheckDefinitionPathHierarchyRoots: lib, spec, test, src
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
@ -176,19 +157,6 @@ Naming/FileName:
Exclude:
- 'config/locales/sr-Latn.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
Naming/MemoizedInstanceVariableName:
Exclude:
- 'app/controllers/api/v1/bookmarks_controller.rb'
- 'app/controllers/api/v1/favourites_controller.rb'
- 'app/controllers/concerns/rate_limit_headers.rb'
- 'app/lib/activitypub/activity.rb'
- 'app/services/resolve_url_service.rb'
- 'app/services/search_service.rb'
- 'config/initializers/rack_attack.rb'
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
# SupportedStyles: snake_case, normalcase, non_integer
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
@ -283,7 +251,6 @@ RSpec/HookArgument:
- 'spec/serializers/activitypub/note_serializer_spec.rb'
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
- 'spec/services/import_service_spec.rb'
- 'spec/spec_helper.rb'
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
@ -398,45 +365,6 @@ RSpec/PendingWithoutReason:
Exclude:
- 'spec/models/account_spec.rb'
RSpec/StubbedMock:
Exclude:
- 'spec/controllers/api/base_controller_spec.rb'
- 'spec/controllers/api/v1/media_controller_spec.rb'
- 'spec/controllers/auth/registrations_controller_spec.rb'
- 'spec/helpers/application_helper_spec.rb'
- 'spec/lib/status_filter_spec.rb'
- 'spec/lib/status_finder_spec.rb'
- 'spec/lib/webfinger_resource_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb'
RSpec/SubjectDeclaration:
Exclude:
- 'spec/controllers/admin/domain_blocks_controller_spec.rb'
- 'spec/models/account_migration_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/relationship_filter_spec.rb'
- 'spec/models/user_role_spec.rb'
- 'spec/policies/account_moderation_note_policy_spec.rb'
- 'spec/policies/account_policy_spec.rb'
- 'spec/policies/backup_policy_spec.rb'
- 'spec/policies/custom_emoji_policy_spec.rb'
- 'spec/policies/domain_block_policy_spec.rb'
- 'spec/policies/email_domain_block_policy_spec.rb'
- 'spec/policies/instance_policy_spec.rb'
- 'spec/policies/invite_policy_spec.rb'
- 'spec/policies/relay_policy_spec.rb'
- 'spec/policies/report_note_policy_spec.rb'
- 'spec/policies/report_policy_spec.rb'
- 'spec/policies/settings_policy_spec.rb'
- 'spec/policies/tag_policy_spec.rb'
- 'spec/policies/user_policy_spec.rb'
- 'spec/services/activitypub/process_account_service_spec.rb'
RSpec/SubjectStub:
Exclude:
- 'spec/services/unallow_domain_service_spec.rb'
- 'spec/validators/blacklisted_email_validator_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/ApplicationController:
Exclude:
@ -776,405 +704,6 @@ Style/FormatStringToken:
- 'config/initializers/devise.rb'
- 'lib/paperclip/color_extractor.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, always_true, never
Style/FrozenStringLiteralComment:
Exclude:
- 'app/views/accounts/show.rss.ruby'
- 'app/views/tags/show.rss.ruby'
- 'app/views/well_known/host_meta/show.xml.ruby'
- 'config/application.rb'
- 'config/boot.rb'
- 'config/environment.rb'
- 'config/environments/development.rb'
- 'config/environments/production.rb'
- 'config/environments/test.rb'
- 'config/initializers/0_post_deployment_migrations.rb'
- 'config/initializers/active_model_serializers.rb'
- 'config/initializers/application_controller_renderer.rb'
- 'config/initializers/assets.rb'
- 'config/initializers/backtrace_silencers.rb'
- 'config/initializers/cache_logging.rb'
- 'config/initializers/chewy.rb'
- 'config/initializers/content_security_policy.rb'
- 'config/initializers/cookies_serializer.rb'
- 'config/initializers/cors.rb'
- 'config/initializers/devise.rb'
- 'config/initializers/doorkeeper.rb'
- 'config/initializers/fast_blank.rb'
- 'config/initializers/ffmpeg.rb'
- 'config/initializers/filter_parameter_logging.rb'
- 'config/initializers/http_client_proxy.rb'
- 'config/initializers/httplog.rb'
- 'config/initializers/inflections.rb'
- 'config/initializers/mail_delivery_job.rb'
- 'config/initializers/mime_types.rb'
- 'config/initializers/oj.rb'
- 'config/initializers/omniauth.rb'
- 'config/initializers/open_uri_redirection.rb'
- 'config/initializers/permissions_policy.rb'
- 'config/initializers/pghero.rb'
- 'config/initializers/preload_link_headers.rb'
- 'config/initializers/premailer_rails.rb'
- 'config/initializers/rack_attack_logging.rb'
- 'config/initializers/redis.rb'
- 'config/initializers/session_store.rb'
- 'config/initializers/simple_form.rb'
- 'config/initializers/stoplight.rb'
- 'config/initializers/trusted_proxies.rb'
- 'config/initializers/twitter_regex.rb'
- 'config/initializers/webauthn.rb'
- 'config/initializers/wrap_parameters.rb'
- 'config/locales/sr-Latn.rb'
- 'config/locales/sr.rb'
- 'config/puma.rb'
- 'db/migrate/20160220174730_create_accounts.rb'
- 'db/migrate/20160220211917_create_statuses.rb'
- 'db/migrate/20160221003140_create_users.rb'
- 'db/migrate/20160221003621_create_follows.rb'
- 'db/migrate/20160222122600_create_stream_entries.rb'
- 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb'
- 'db/migrate/20160223162837_add_metadata_to_statuses.rb'
- 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb'
- 'db/migrate/20160223165723_add_url_to_statuses.rb'
- 'db/migrate/20160223165855_add_url_to_accounts.rb'
- 'db/migrate/20160223171800_create_favourites.rb'
- 'db/migrate/20160224223247_create_mentions.rb'
- 'db/migrate/20160227230233_add_attachment_avatar_to_accounts.rb'
- 'db/migrate/20160305115639_add_devise_to_users.rb'
- 'db/migrate/20160306172223_create_doorkeeper_tables.rb'
- 'db/migrate/20160312193225_add_attachment_header_to_accounts.rb'
- 'db/migrate/20160314164231_add_owner_to_application.rb'
- 'db/migrate/20160316103650_add_missing_indices.rb'
- 'db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb'
- 'db/migrate/20160325130944_add_admin_to_users.rb'
- 'db/migrate/20160826155805_add_superapp_to_oauth_applications.rb'
- 'db/migrate/20160905150353_create_media_attachments.rb'
- 'db/migrate/20160919221059_add_subscription_expires_at_to_accounts.rb'
- 'db/migrate/20160920003904_remove_verify_token_from_accounts.rb'
- 'db/migrate/20160926213048_remove_owner_from_application.rb'
- 'db/migrate/20161003142332_add_confirmable_to_users.rb'
- 'db/migrate/20161003145426_create_blocks.rb'
- 'db/migrate/20161006213403_rails_settings_migration.rb'
- 'db/migrate/20161009120834_create_domain_blocks.rb'
- 'db/migrate/20161027172456_add_silenced_to_accounts.rb'
- 'db/migrate/20161104173623_create_tags.rb'
- 'db/migrate/20161105130633_create_statuses_tags_join_table.rb'
- 'db/migrate/20161116162355_add_locale_to_users.rb'
- 'db/migrate/20161119211120_create_notifications.rb'
- 'db/migrate/20161122163057_remove_unneeded_indexes.rb'
- 'db/migrate/20161123093447_add_sensitive_to_statuses.rb'
- 'db/migrate/20161128103007_create_subscriptions.rb'
- 'db/migrate/20161130142058_add_last_successful_delivery_at_to_subscriptions.rb'
- 'db/migrate/20161130185319_add_visibility_to_statuses.rb'
- 'db/migrate/20161202132159_add_in_reply_to_account_id_to_statuses.rb'
- 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb'
- 'db/migrate/20161205214545_add_suspended_to_accounts.rb'
- 'db/migrate/20161221152630_add_hidden_to_stream_entries.rb'
- 'db/migrate/20161222201034_add_locked_to_accounts.rb'
- 'db/migrate/20161222204147_create_follow_requests.rb'
- 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb'
- 'db/migrate/20170109120109_create_web_settings.rb'
- 'db/migrate/20170112154826_migrate_settings.rb'
- 'db/migrate/20170114194937_add_application_to_statuses.rb'
- 'db/migrate/20170114203041_add_website_to_oauth_application.rb'
- 'db/migrate/20170119214911_create_preview_cards.rb'
- 'db/migrate/20170123162658_add_severity_to_domain_blocks.rb'
- 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb'
- 'db/migrate/20170125145934_add_spoiler_text_to_statuses.rb'
- 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb'
- 'db/migrate/20170205175257_remove_devices.rb'
- 'db/migrate/20170209184350_add_reply_to_statuses.rb'
- 'db/migrate/20170214110202_create_reports.rb'
- 'db/migrate/20170217012631_add_reblog_of_id_foreign_key_to_statuses.rb'
- 'db/migrate/20170301222600_create_mutes.rb'
- 'db/migrate/20170303212857_add_last_emailed_at_to_users.rb'
- 'db/migrate/20170304202101_add_type_to_media_attachments.rb'
- 'db/migrate/20170317193015_add_search_index_to_accounts.rb'
- 'db/migrate/20170318214217_add_header_remote_url_to_accounts.rb'
- 'db/migrate/20170322021028_add_lowercase_index_to_accounts.rb'
- 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb'
- 'db/migrate/20170322162804_add_search_index_to_tags.rb'
- 'db/migrate/20170330021336_add_counter_caches.rb'
- 'db/migrate/20170330163835_create_imports.rb'
- 'db/migrate/20170330164118_add_attachment_data_to_imports.rb'
- 'db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb'
- 'db/migrate/20170405112956_add_index_on_mentions_status_id.rb'
- 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb'
- 'db/migrate/20170409170753_add_last_webfingered_at_to_accounts.rb'
- 'db/migrate/20170414080609_add_devise_two_factor_backupable_to_users.rb'
- 'db/migrate/20170414132105_add_language_to_statuses.rb'
- 'db/migrate/20170418160728_add_indexes_to_reports_for_accounts.rb'
- 'db/migrate/20170423005413_add_allowed_languages_to_user.rb'
- 'db/migrate/20170424003227_create_account_domain_blocks.rb'
- 'db/migrate/20170424112722_add_status_id_index_to_statuses_tags.rb'
- 'db/migrate/20170425131920_add_media_attachment_meta.rb'
- 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb'
- 'db/migrate/20170427011934_re_add_owner_to_application.rb'
- 'db/migrate/20170506235850_create_conversations.rb'
- 'db/migrate/20170507000211_add_conversation_id_to_statuses.rb'
- 'db/migrate/20170507141759_optimize_index_subscriptions.rb'
- 'db/migrate/20170508230434_create_conversation_mutes.rb'
- 'db/migrate/20170516072309_add_index_accounts_on_uri.rb'
- 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb'
- 'db/migrate/20170601210557_add_index_on_media_attachments_account_id.rb'
- 'db/migrate/20170604144747_add_foreign_keys_for_accounts.rb'
- 'db/migrate/20170606113804_change_tag_search_index_to_btree.rb'
- 'db/migrate/20170609145826_remove_default_language_from_statuses.rb'
- 'db/migrate/20170610000000_add_statuses_index_on_account_id_id.rb'
- 'db/migrate/20170623152212_create_session_activations.rb'
- 'db/migrate/20170624134742_add_description_to_session_activations.rb'
- 'db/migrate/20170625140443_add_access_token_id_to_session_activations.rb'
- 'db/migrate/20170711225116_fix_null_booleans.rb'
- 'db/migrate/20170713112503_make_tag_search_case_insensitive.rb'
- 'db/migrate/20170713175513_create_web_push_subscriptions.rb'
- 'db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb'
- 'db/migrate/20170714184731_add_domain_to_subscriptions.rb'
- 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb'
- 'db/migrate/20170718211102_add_activitypub_to_accounts.rb'
- 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb'
- 'db/migrate/20170823162448_create_status_pins.rb'
- 'db/migrate/20170824103029_add_timestamps_to_status_pins.rb'
- 'db/migrate/20170829215220_remove_status_pins_account_index.rb'
- 'db/migrate/20170901141119_truncate_preview_cards.rb'
- 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb'
- 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb'
- 'db/migrate/20170905165803_add_local_to_statuses.rb'
- 'db/migrate/20170913000752_create_site_uploads.rb'
- 'db/migrate/20170917153509_create_custom_emojis.rb'
- 'db/migrate/20170918125918_ids_to_bigints.rb'
- 'db/migrate/20170920024819_status_ids_to_timestamp_ids.rb'
- 'db/migrate/20170920032311_fix_reblogs_in_feeds.rb'
- 'db/migrate/20170924022025_ids_to_bigints2.rb'
- 'db/migrate/20170927215609_add_description_to_media_attachments.rb'
- 'db/migrate/20170928082043_create_email_domain_blocks.rb'
- 'db/migrate/20171005102658_create_account_moderation_notes.rb'
- 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb'
- 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb'
- 'db/migrate/20171010023049_add_foreign_key_to_account_moderation_notes.rb'
- 'db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb'
- 'db/migrate/20171020084748_add_visible_in_picker_to_custom_emoji.rb'
- 'db/migrate/20171028221157_add_reblogs_to_follows.rb'
- 'db/migrate/20171107143332_add_memorial_to_accounts.rb'
- 'db/migrate/20171107143624_add_disabled_to_users.rb'
- 'db/migrate/20171109012327_add_moderator_to_accounts.rb'
- 'db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb'
- 'db/migrate/20171114231651_create_lists.rb'
- 'db/migrate/20171116161857_create_list_accounts.rb'
- 'db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb'
- 'db/migrate/20171119172437_create_admin_action_logs.rb'
- 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb'
- 'db/migrate/20171125024930_create_invites.rb'
- 'db/migrate/20171125031751_add_invite_id_to_users.rb'
- 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb'
- 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb'
- 'db/migrate/20171129172043_add_index_on_stream_entries.rb'
- 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb'
- 'db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb'
- 'db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb'
- 'db/migrate/20171226094803_more_faster_index_on_notifications.rb'
- 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
- 'db/migrate/20180109143959_add_remember_token_to_users.rb'
- 'db/migrate/20180204034416_create_identities.rb'
- 'db/migrate/20180206000000_change_user_id_nonnullable.rb'
- 'db/migrate/20180211015820_create_backups.rb'
- 'db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb'
- 'db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb'
- 'db/migrate/20180402031200_add_assigned_account_id_to_reports.rb'
- 'db/migrate/20180402040909_create_report_notes.rb'
- 'db/migrate/20180410204633_add_fields_to_accounts.rb'
- 'db/migrate/20180416210259_add_uri_to_relationships.rb'
- 'db/migrate/20180506221944_add_actor_type_to_accounts.rb'
- 'db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb'
- 'db/migrate/20180510230049_migrate_web_push_subscriptions.rb'
- 'db/migrate/20180528141303_fix_accounts_unique_index.rb'
- 'db/migrate/20180608213548_reject_following_blocked_users.rb'
- 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb'
- 'db/migrate/20180615122121_add_autofollow_to_invites.rb'
- 'db/migrate/20180616192031_add_chosen_languages_to_users.rb'
- 'db/migrate/20180617162849_remove_unused_indexes.rb'
- 'db/migrate/20180628181026_create_custom_filters.rb'
- 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb'
- 'db/migrate/20180711152640_create_relays.rb'
- 'db/migrate/20180808175627_create_account_pins.rb'
- 'db/migrate/20180812123222_change_relays_enabled.rb'
- 'db/migrate/20180812162710_create_status_stats.rb'
- 'db/migrate/20180812173710_copy_status_stats.rb'
- 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb'
- 'db/migrate/20180831171112_create_bookmarks.rb'
- 'db/migrate/20180929222014_create_account_conversations.rb'
- 'db/migrate/20181007025445_create_pghero_space_stats.rb'
- 'db/migrate/20181010141500_add_silent_to_mentions.rb'
- 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb'
- 'db/migrate/20181018205649_add_unread_to_account_conversations.rb'
- 'db/migrate/20181024224956_migrate_account_conversations.rb'
- 'db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb'
- 'db/migrate/20181116165755_create_account_stats.rb'
- 'db/migrate/20181116173541_copy_account_stats.rb'
- 'db/migrate/20181127130500_identity_id_to_bigint.rb'
- 'db/migrate/20181127165847_add_show_replies_to_lists.rb'
- 'db/migrate/20181203003808_create_accounts_tags_join_table.rb'
- 'db/migrate/20181203021853_add_discoverable_to_accounts.rb'
- 'db/migrate/20181204193439_add_last_status_at_to_account_stats.rb'
- 'db/migrate/20181204215309_create_account_tag_stats.rb'
- 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb'
- 'db/migrate/20181213184704_create_account_warnings.rb'
- 'db/migrate/20181213185533_create_account_warning_presets.rb'
- 'db/migrate/20181219235220_add_created_by_application_id_to_users.rb'
- 'db/migrate/20181226021420_add_also_known_as_to_accounts.rb'
- 'db/migrate/20190103124649_create_scheduled_statuses.rb'
- 'db/migrate/20190103124754_add_scheduled_status_id_to_media_attachments.rb'
- 'db/migrate/20190117114553_create_tombstones.rb'
- 'db/migrate/20190201012802_add_overwrite_to_imports.rb'
- 'db/migrate/20190203180359_create_featured_tags.rb'
- 'db/migrate/20190225031541_create_polls.rb'
- 'db/migrate/20190225031625_create_poll_votes.rb'
- 'db/migrate/20190226003449_add_poll_id_to_statuses.rb'
- 'db/migrate/20190304152020_add_uri_to_poll_votes.rb'
- 'db/migrate/20190306145741_add_lock_version_to_polls.rb'
- 'db/migrate/20190307234537_add_approved_to_users.rb'
- 'db/migrate/20190314181829_migrate_open_registrations_setting.rb'
- 'db/migrate/20190316190352_create_account_identity_proofs.rb'
- 'db/migrate/20190317135723_add_uri_to_reports.rb'
- 'db/migrate/20190403141604_add_comment_to_invites.rb'
- 'db/migrate/20190409054914_create_user_invite_requests.rb'
- 'db/migrate/20190420025523_add_blurhash_to_media_attachments.rb'
- 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb'
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
- 'db/migrate/20190627222225_create_custom_emoji_categories.rb'
- 'db/migrate/20190627222826_add_category_id_to_custom_emojis.rb'
- 'db/migrate/20190701022101_add_trust_level_to_accounts.rb'
- 'db/migrate/20190705002136_create_domain_allows.rb'
- 'db/migrate/20190715164535_add_instance_actor.rb'
- 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb'
- 'db/migrate/20190729185330_add_score_to_tags.rb'
- 'db/migrate/20190805123746_add_capabilities_to_tags.rb'
- 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb'
- 'db/migrate/20190815225426_add_last_status_at_to_tags.rb'
- 'db/migrate/20190819134503_add_deleted_at_to_statuses.rb'
- 'db/migrate/20190820003045_update_statuses_index.rb'
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
- 'db/migrate/20190901035623_add_max_score_to_tags.rb'
- 'db/migrate/20190904222339_create_markers.rb'
- 'db/migrate/20190914202517_create_account_migrations.rb'
- 'db/migrate/20190915194355_create_account_aliases.rb'
- 'db/migrate/20190927232842_add_voters_count_to_polls.rb'
- 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb'
- 'db/migrate/20191007013357_update_pt_locales.rb'
- 'db/migrate/20191031163205_change_list_account_follow_nullable.rb'
- 'db/migrate/20191212003415_increase_backup_size.rb'
- 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb'
- 'db/migrate/20191218153258_create_announcements.rb'
- 'db/migrate/20200113125135_create_announcement_mutes.rb'
- 'db/migrate/20200114113335_create_announcement_reactions.rb'
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
- 'db/migrate/20200126203551_add_published_at_to_announcements.rb'
- 'db/migrate/20200306035625_add_processing_to_media_attachments.rb'
- 'db/migrate/20200309150742_add_forwarded_to_reports.rb'
- 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb'
- 'db/migrate/20200312162302_add_status_ids_to_announcements.rb'
- 'db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb'
- 'db/migrate/20200317021758_add_expires_at_to_mutes.rb'
- 'db/migrate/20200407201300_create_unavailable_domains.rb'
- 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
- 'db/migrate/20200417125749_add_storage_schema_version.rb'
- 'db/migrate/20200508212852_reset_unique_jobs_locks.rb'
- 'db/migrate/20200510110808_reset_web_app_secret.rb'
- 'db/migrate/20200510181721_remove_duplicated_indexes_pghero.rb'
- 'db/migrate/20200516180352_create_devices.rb'
- 'db/migrate/20200516183822_create_one_time_keys.rb'
- 'db/migrate/20200518083523_create_encrypted_messages.rb'
- 'db/migrate/20200521180606_encrypted_message_ids_to_timestamp_ids.rb'
- 'db/migrate/20200529214050_add_devices_url_to_accounts.rb'
- 'db/migrate/20200601222558_create_system_keys.rb'
- 'db/migrate/20200605155027_add_blurhash_to_preview_cards.rb'
- 'db/migrate/20200608113046_add_sign_in_token_to_users.rb'
- 'db/migrate/20200614002136_add_sensitized_to_accounts.rb'
- 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb'
- 'db/migrate/20200622213645_media_attachment_ids_to_timestamp_ids.rb'
- 'db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb'
- 'db/migrate/20200628133322_create_account_notes.rb'
- 'db/migrate/20200630190240_create_webauthn_credentials.rb'
- 'db/migrate/20200630190544_add_webauthn_id_to_users.rb'
- 'db/migrate/20200908193330_create_account_deletion_requests.rb'
- 'db/migrate/20200917192924_add_notify_to_follows.rb'
- 'db/migrate/20200917193034_add_type_to_notifications.rb'
- 'db/migrate/20200917222316_add_index_notifications_on_type.rb'
- 'db/migrate/20201008202037_create_ip_blocks.rb'
- 'db/migrate/20201008220312_add_sign_up_ip_to_users.rb'
- 'db/migrate/20201017233919_add_suspension_origin_to_accounts.rb'
- 'db/migrate/20201206004238_create_instances.rb'
- 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb'
- 'db/migrate/20210221045109_create_rules.rb'
- 'db/migrate/20210306164523_account_ids_to_timestamp_ids.rb'
- 'db/migrate/20210322164601_create_account_summaries.rb'
- 'db/migrate/20210323114347_create_follow_recommendations.rb'
- 'db/migrate/20210324171613_create_follow_recommendation_suppressions.rb'
- 'db/migrate/20210416200740_create_canonical_email_blocks.rb'
- 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb'
- 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb'
- 'db/migrate/20210505174616_update_follow_recommendations_to_version_2.rb'
- 'db/migrate/20210609202149_create_login_activities.rb'
- 'db/migrate/20210616214526_create_user_ips.rb'
- 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb'
- 'db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb'
- 'db/migrate/20210722120340_create_account_statuses_cleanup_policies.rb'
- 'db/migrate/20210904215403_add_edited_at_to_statuses.rb'
- 'db/migrate/20210908220918_create_status_edits.rb'
- 'db/migrate/20211031031021_create_preview_card_providers.rb'
- 'db/migrate/20211112011713_add_language_to_preview_cards.rb'
- 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb'
- 'db/migrate/20211123212714_add_link_type_to_preview_cards.rb'
- 'db/migrate/20211213040746_update_account_summaries_to_version_2.rb'
- 'db/migrate/20211231080958_add_category_to_reports.rb'
- 'db/migrate/20220105163928_remove_mentions_status_id_index.rb'
- 'db/migrate/20220115125126_add_report_id_to_account_warnings.rb'
- 'db/migrate/20220115125341_fix_account_warning_actions.rb'
- 'db/migrate/20220116202951_add_deleted_at_index_on_statuses.rb'
- 'db/migrate/20220124141035_create_appeals.rb'
- 'db/migrate/20220202200743_add_trendable_to_accounts.rb'
- 'db/migrate/20220202200926_add_trendable_to_statuses.rb'
- 'db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb'
- 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb'
- 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb'
- 'db/migrate/20220302232632_add_ordered_media_attachment_ids_to_statuses.rb'
- 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb'
- 'db/migrate/20220304195405_migrate_hide_network_preference.rb'
- 'db/migrate/20220307094650_fix_featured_tags_constraints.rb'
- 'db/migrate/20220309213005_fix_reblog_deleted_at.rb'
- 'db/migrate/20220316233212_update_kurdish_locales.rb'
- 'db/migrate/20220428112511_add_index_statuses_on_account_id.rb'
- 'db/migrate/20220428112727_add_index_statuses_pins_on_status_id.rb'
- 'db/migrate/20220428114454_add_index_reports_on_assigned_account_id.rb'
- 'db/migrate/20220428114902_add_index_reports_on_action_taken_by_account_id.rb'
- 'db/migrate/20220606044941_create_webhooks.rb'
- 'db/migrate/20220611210335_create_user_roles.rb'
- 'db/migrate/20220611212541_add_role_id_to_users.rb'
- 'db/migrate/20220710102457_add_display_name_to_tags.rb'
- 'db/migrate/20220714171049_create_tag_follows.rb'
- 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb'
- 'db/migrate/20220824233535_create_status_trends.rb'
- 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb'
- 'db/migrate/20220829192633_add_languages_to_follows.rb'
- 'db/migrate/20220829192658_add_languages_to_follow_requests.rb'
- 'db/migrate/20221006061337_create_preview_card_trends.rb'
- 'db/migrate/20221012181003_add_blurhash_to_site_uploads.rb'
- 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb'
- 'db/migrate/20221025171544_add_index_ip_blocks_on_ip.rb'
- 'db/migrate/20221104133904_add_name_to_featured_tags.rb'
- 'db/post_migrate/20190519130537_remove_boosts_widening_audience.rb'
- 'db/post_migrate/20210308133107_remove_subscription_expires_at_from_accounts.rb'
- 'db/post_migrate/20220118183123_remove_rememberable_from_users.rb'
- 'db/seeds/01_web_app.rb'
- 'db/seeds/02_instance_actor.rb'
- 'db/seeds/03_roles.rb'
- 'db/seeds/04_admin.rb'
- 'lib/rails/engine_extensions.rb'
- 'lib/tasks/branding.rake'
- 'spec/fabricators_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream:
Exclude:
@ -1336,13 +865,6 @@ Style/SafeNavigation:
- 'app/models/concerns/account_finder_concern.rb'
- 'app/models/status.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Exclude:
- 'spec/services/activitypub/process_status_update_service_spec.rb'
- 'spec/validators/blacklisted_email_validator_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: only_raise, only_fail, semantic
@ -1356,21 +878,6 @@ Style/SingleArgumentDig:
Exclude:
- 'lib/webpacker/manifest_extensions.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/SlicingWithRange:
Exclude:
- 'app/lib/emoji_formatter.rb'
- 'app/lib/text_formatter.rb'
- 'app/models/account_alias.rb'
- 'app/models/domain_block.rb'
- 'app/models/email_domain_block.rb'
- 'app/models/preview_card_provider.rb'
- 'app/validators/status_length_validator.rb'
- 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb'
- 'lib/active_record/batches.rb'
- 'lib/mastodon/premailer_webpack_strategy.rb'
- 'lib/tasks/repo.rake'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_parentheses, require_no_parentheses

View File

@ -143,7 +143,7 @@ All notable changes to this project will be documented in this file.
- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833))
- Add setting for status page URL ([Gargron](https://github.com/mastodon/mastodon/pull/23390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23499))
- REST API changes:
- Add `configuration.urls.status` attribute to the object returned by `GET /api/v1/instance`
- Add `configuration.urls.status` attribute to the object returned by `GET /api/v2/instance`
- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938))
- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131))
- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895))

View File

@ -4,7 +4,7 @@ source 'https://rubygems.org'
ruby '>= 3.0.0'
gem 'puma', '~> 6.3'
gem 'rails', '~> 6.1.7'
gem 'rails', '~> 7.0'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.7'
@ -66,7 +66,7 @@ gem 'pundit', '~> 2.3'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
gem 'rails-i18n', '~> 6.0'
gem 'rails-i18n', '~> 7.0'
gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true'
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
@ -158,7 +158,7 @@ group :development do
gem 'letter_opener_web', '~> 2.0'
# Security analysis CLI tools
gem 'brakeman', '~> 5.4', require: false
gem 'brakeman', '~> 6.0', require: false
gem 'bundler-audit', '~> 0.9', require: false
# Linter CLI for HAML files

View File

@ -18,40 +18,47 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (6.1.7.4)
actionpack (= 6.1.7.4)
activesupport (= 6.1.7.4)
actioncable (7.0.6)
actionpack (= 7.0.6)
activesupport (= 7.0.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.4)
actionpack (= 6.1.7.4)
activejob (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
actionmailbox (7.0.6)
actionpack (= 7.0.6)
activejob (= 7.0.6)
activerecord (= 7.0.6)
activestorage (= 7.0.6)
activesupport (= 7.0.6)
mail (>= 2.7.1)
actionmailer (6.1.7.4)
actionpack (= 6.1.7.4)
actionview (= 6.1.7.4)
activejob (= 6.1.7.4)
activesupport (= 6.1.7.4)
net-imap
net-pop
net-smtp
actionmailer (7.0.6)
actionpack (= 7.0.6)
actionview (= 7.0.6)
activejob (= 7.0.6)
activesupport (= 7.0.6)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0)
actionpack (6.1.7.4)
actionview (= 6.1.7.4)
activesupport (= 6.1.7.4)
rack (~> 2.0, >= 2.0.9)
actionpack (7.0.6)
actionview (= 7.0.6)
activesupport (= 7.0.6)
rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.7.4)
actionpack (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
actiontext (7.0.6)
actionpack (= 7.0.6)
activerecord (= 7.0.6)
activestorage (= 7.0.6)
activesupport (= 7.0.6)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (6.1.7.4)
activesupport (= 6.1.7.4)
actionview (7.0.6)
activesupport (= 7.0.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -61,27 +68,26 @@ GEM
activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (6.1.7.4)
activesupport (= 6.1.7.4)
activejob (7.0.6)
activesupport (= 7.0.6)
globalid (>= 0.3.6)
activemodel (6.1.7.4)
activesupport (= 6.1.7.4)
activerecord (6.1.7.4)
activemodel (= 6.1.7.4)
activesupport (= 6.1.7.4)
activestorage (6.1.7.4)
actionpack (= 6.1.7.4)
activejob (= 6.1.7.4)
activerecord (= 6.1.7.4)
activesupport (= 6.1.7.4)
activemodel (7.0.6)
activesupport (= 7.0.6)
activerecord (7.0.6)
activemodel (= 7.0.6)
activesupport (= 7.0.6)
activestorage (7.0.6)
actionpack (= 7.0.6)
activejob (= 7.0.6)
activerecord (= 7.0.6)
activesupport (= 7.0.6)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
activesupport (6.1.7.4)
activesupport (7.0.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
@ -130,7 +136,7 @@ GEM
blurhash (0.1.7)
bootsnap (1.16.0)
msgpack (~> 1.2)
brakeman (5.4.1)
brakeman (6.0.0)
browser (5.3.1)
brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5)
@ -146,7 +152,7 @@ GEM
sshkit (>= 1.9.0)
capistrano-bundler (2.1.0)
capistrano (~> 3.1)
capistrano-rails (1.6.2)
capistrano-rails (1.6.3)
capistrano (~> 3.1)
capistrano-bundler (>= 1.1, < 3)
capistrano-rbenv (2.2.0)
@ -167,7 +173,7 @@ GEM
activesupport
cbor (0.5.9.6)
charlock_holmes (0.7.7)
chewy (7.3.2)
chewy (7.3.3)
activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl
@ -291,11 +297,11 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.45.0)
haml_lint (0.48.0)
haml (>= 4.0, < 6.2)
parallel (~> 1.10)
rainbow
rubocop (>= 0.50.0)
rubocop (>= 1.0)
sysexits (~> 1.1)
hashdiff (1.0.1)
hashie (5.0.0)
@ -373,6 +379,7 @@ GEM
marcel (~> 1.0.1)
mime-types
terrapin (~> 0.6.0)
language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
letter_opener (1.8.1)
@ -508,21 +515,20 @@ GEM
rack
rack-test (2.1.0)
rack (>= 1.3)
rails (6.1.7.4)
actioncable (= 6.1.7.4)
actionmailbox (= 6.1.7.4)
actionmailer (= 6.1.7.4)
actionpack (= 6.1.7.4)
actiontext (= 6.1.7.4)
actionview (= 6.1.7.4)
activejob (= 6.1.7.4)
activemodel (= 6.1.7.4)
activerecord (= 6.1.7.4)
activestorage (= 6.1.7.4)
activesupport (= 6.1.7.4)
rails (7.0.6)
actioncable (= 7.0.6)
actionmailbox (= 7.0.6)
actionmailer (= 7.0.6)
actionpack (= 7.0.6)
actiontext (= 7.0.6)
actionview (= 7.0.6)
activejob (= 7.0.6)
activemodel (= 7.0.6)
activerecord (= 7.0.6)
activestorage (= 7.0.6)
activesupport (= 7.0.6)
bundler (>= 1.15.0)
railties (= 6.1.7.4)
sprockets-rails (>= 2.0.0)
railties (= 7.0.6)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@ -533,15 +539,16 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails-i18n (6.0.0)
rails-i18n (7.0.7)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
railties (6.1.7.4)
actionpack (= 6.1.7.4)
activesupport (= 6.1.7.4)
railties (>= 6.0.0, < 8)
railties (7.0.6)
actionpack (= 7.0.6)
activesupport (= 7.0.6)
method_source
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
rdf (3.2.11)
@ -589,8 +596,9 @@ GEM
sidekiq (>= 2.4.0)
rspec-support (3.12.0)
rspec_chunked (0.6)
rubocop (1.52.1)
rubocop (1.54.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0)
@ -608,7 +616,7 @@ GEM
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.19.1)
rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
@ -668,7 +676,7 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sshkit (1.21.4)
sshkit (1.21.5)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.25)
@ -688,7 +696,7 @@ GEM
climate_control (>= 0.0.3, < 1.0)
thor (1.2.2)
tilt (2.2.0)
timeout (0.3.2)
timeout (0.4.0)
tpm-key_attestation (0.12.0)
bindata (~> 2.4)
openssl (> 2.0)
@ -765,7 +773,7 @@ DEPENDENCIES
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.16.0)
brakeman (~> 5.4)
brakeman (~> 6.0)
browser
bundler-audit (~> 0.9)
capistrano (~> 3.17)
@ -839,9 +847,9 @@ DEPENDENCIES
rack-attack (~> 6.6)
rack-cors (~> 2.0)
rack-test (~> 2.1)
rails (~> 6.1.7)
rails (~> 7.0)
rails-controller-testing (~> 1.0)
rails-i18n (~> 6.0)
rails-i18n (~> 7.0)
rails-settings-cached (~> 0.6)!
rdf-normalize (~> 0.5)
redcarpet (~> 3.6)

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
require File.expand_path('config/application', __dir__)
Rails.application.load_tasks

View File

@ -21,7 +21,7 @@ class Api::V1::BookmarksController < Api::BaseController
end
def results
@_results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
@results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)

View File

@ -21,7 +21,7 @@ class Api::V1::FavouritesController < Api::BaseController
end
def results
@_results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
@results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)

View File

@ -7,7 +7,10 @@ class Api::V1::MarkersController < Api::BaseController
before_action :require_user!
def index
@markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline)
with_read_replica do
@markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline)
end
render json: serialize_map(@markers)
end

View File

@ -9,8 +9,12 @@ class Api::V1::NotificationsController < Api::BaseController
DEFAULT_NOTIFICATIONS_LIMIT = 40
def index
@notifications = load_notifications
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
with_read_replica do
@notifications = load_notifications
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
end
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
end
def show

View File

@ -23,6 +23,6 @@ class Api::V1::ReportsController < Api::BaseController
end
def report_params
params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: [])
end
end

View File

@ -6,7 +6,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
ApplicationRecord.connected_to(role: :read, prevent_writes: true) do
with_read_replica do
@statuses = load_statuses
@relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end

View File

@ -1,25 +1,36 @@
# frozen_string_literal: true
class Api::Web::EmbedsController < Api::Web::BaseController
before_action :require_user!
include Authorization
def create
status = StatusFinder.new(params[:url]).status
before_action :set_status
return not_found if status.hidden?
def show
return not_found if @status.hidden?
render json: status, serializer: OEmbedSerializer, width: 400
rescue ActiveRecord::RecordNotFound
oembed = FetchOEmbedService.new.call(params[:url])
if @status.local?
render json: @status, serializer: OEmbedSerializer, width: 400
else
return not_found unless user_signed_in?
return not_found if oembed.nil?
url = ActivityPub::TagManager.instance.url_for(@status)
oembed = FetchOEmbedService.new.call(url)
return not_found if oembed.nil?
begin
oembed[:html] = Sanitize.fragment(oembed[:html], Sanitize::Config::MASTODON_OEMBED)
rescue ArgumentError
return not_found
begin
oembed[:html] = Sanitize.fragment(oembed[:html], Sanitize::Config::MASTODON_OEMBED)
rescue ArgumentError
return not_found
end
render json: oembed
end
end
render json: oembed
def set_status
@status = Status.find(params[:id])
authorize @status, :show?
rescue Mastodon::NotPermittedError
not_found
end
end

View File

@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base
include SessionTrackingConcern
include CacheConcern
include DomainControlHelper
include DatabaseHelper
helper_method :current_account
helper_method :current_session

View File

@ -124,7 +124,7 @@ class Auth::SessionsController < Devise::SessionsController
redirect_to new_user_session_path, alert: I18n.t('devise.failure.timeout')
end
def set_attempt_session(user)
def register_attempt_in_session(user)
session[:attempt_user_id] = user.id
session[:attempt_user_updated_at] = user.updated_at.to_s
end

View File

@ -61,7 +61,7 @@ module RateLimitHeaders
end
def request_time
@_request_time ||= Time.now.utc
@request_time ||= Time.now.utc
end
def reset_period_offset

View File

@ -75,7 +75,7 @@ module TwoFactorAuthenticationConcern
end
def prompt_for_two_factor(user)
set_attempt_session(user)
register_attempt_in_session(user)
@body_classes = 'lighter'
@webauthn_enabled = user.webauthn_enabled?

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module DatabaseHelper
def with_read_replica(&block)
ApplicationRecord.connected_to(role: :read, prevent_writes: true, &block)
end
def with_primary(&block)
ApplicationRecord.connected_to(role: :primary, &block)
end
end

View File

@ -2,7 +2,7 @@
module DomainControlHelper
def domain_not_allowed?(uri_or_domain)
return if uri_or_domain.blank?
return false if uri_or_domain.blank?
domain = if uri_or_domain.include?('://')
Addressable::URI.parse(uri_or_domain).host

View File

@ -86,10 +86,9 @@ const DIGIT_CHARACTERS = [
export const decode83 = (str: string) => {
let value = 0;
let c, digit;
let digit;
for (let i = 0; i < str.length; i++) {
c = str[i];
for (const c of str) {
digit = DIGIT_CHARACTERS.indexOf(c);
value = value * 83 + digit;
}

View File

@ -32,7 +32,7 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
const willLeave = useCallback(
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
[direction]
[direction],
);
if (reduceMotion) {

View File

@ -6,11 +6,11 @@ interface Props {
tag: {
name: string;
url?: string;
history?: Array<{
history?: {
uses: number;
accounts: string;
day: string;
}>;
}[];
following?: boolean;
type: 'hashtag';
};

View File

@ -5,7 +5,7 @@ import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
interface Props {
account: Account;
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size: number;
style?: React.CSSProperties;
inline?: boolean;

View File

@ -3,8 +3,8 @@ import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
interface Props {
account: Account;
friend: Account;
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size?: number;
baseSize?: number;
overlaySize?: number;

View File

@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
export const StatusesCounter = (
displayNumber: React.ReactNode,
pluralReady: number
pluralReady: number,
) => (
<FormattedMessage
id='account.statuses_counter'
@ -18,7 +18,7 @@ export const StatusesCounter = (
export const FollowingCounter = (
displayNumber: React.ReactNode,
pluralReady: number
pluralReady: number,
) => (
<FormattedMessage
id='account.following_counter'
@ -32,7 +32,7 @@ export const FollowingCounter = (
export const FollowersCounter = (
displayNumber: React.ReactNode,
pluralReady: number
pluralReady: number,
) => (
<FormattedMessage
id='account.followers_counter'

View File

@ -78,7 +78,7 @@ export class DisplayName extends React.PureComponent<Props> {
} else if (account) {
let acct = account.get('acct');
if (acct.indexOf('@') === -1 && localDomain) {
if (!acct.includes('@') && localDomain) {
acct = `${acct}@${localDomain}`;
}

View File

@ -32,7 +32,7 @@ export const GIFV: React.FC<Props> = ({
onClick();
}
},
[onClick]
[onClick],
);
return (

View File

@ -108,7 +108,7 @@ export const timeAgoString = (
now: number,
year: number,
timeGiven: boolean,
short?: boolean
short?: boolean,
) => {
const delta = now - date.getTime();
@ -118,28 +118,28 @@ export const timeAgoString = (
relativeTime = intl.formatMessage(messages.today);
} else if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(
short ? messages.just_now : messages.just_now_full
short ? messages.just_now : messages.just_now_full,
);
} else if (delta < 7 * DAY) {
if (delta < MINUTE) {
relativeTime = intl.formatMessage(
short ? messages.seconds : messages.seconds_full,
{ number: Math.floor(delta / SECOND) }
{ number: Math.floor(delta / SECOND) },
);
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(
short ? messages.minutes : messages.minutes_full,
{ number: Math.floor(delta / MINUTE) }
{ number: Math.floor(delta / MINUTE) },
);
} else if (delta < DAY) {
relativeTime = intl.formatMessage(
short ? messages.hours : messages.hours_full,
{ number: Math.floor(delta / HOUR) }
{ number: Math.floor(delta / HOUR) },
);
} else {
relativeTime = intl.formatMessage(
short ? messages.days : messages.days_full,
{ number: Math.floor(delta / DAY) }
{ number: Math.floor(delta / DAY) },
);
}
} else if (date.getFullYear() === year) {
@ -158,7 +158,7 @@ const timeRemainingString = (
intl: IntlShape,
date: Date,
now: number,
timeGiven = true
timeGiven = true,
) => {
const delta = date.getTime() - now;

View File

@ -0,0 +1,23 @@
import type { PropsWithChildren } from 'react';
import React from 'react';
import type { History } from 'history';
import { createBrowserHistory } from 'history';
import { Router as OriginalRouter } from 'react-router';
import { layoutFromWindow } from 'mastodon/is_mobile';
const browserHistory = createBrowserHistory();
const originalPush = browserHistory.push.bind(browserHistory);
browserHistory.push = (path: string, state: History.LocationState) => {
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
originalPush(`/deck${path}`, state);
} else {
originalPush(path, state);
}
};
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
};

View File

@ -6,7 +6,7 @@ import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
type ShortNumberRenderer = (
displayNumber: JSX.Element,
pluralReady: number
pluralReady: number,
) => JSX.Element;
interface ShortNumberProps {
@ -25,16 +25,16 @@ export const ShortNumberRenderer: React.FC<ShortNumberProps> = ({
if (children && renderer) {
console.warn(
'Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.'
'Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.',
);
}
const customRenderer = children || renderer || null;
const customRenderer = children ?? renderer ?? null;
const displayNumber = <ShortNumberCounter value={shortNumber} />;
return (
customRenderer?.(displayNumber, pluralReady(value, division)) ||
customRenderer?.(displayNumber, pluralReady(value, division)) ??
displayNumber
);
};

View File

@ -258,7 +258,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
}
if (publicStatus) {
if (publicStatus && (signedIn || !isRemote)) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { Helmet } from 'react-helmet';
import { BrowserRouter, Route } from 'react-router-dom';
import { Route } from 'react-router-dom';
import { Provider as ReduxProvider } from 'react-redux';
@ -12,6 +12,7 @@ import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
import { hydrateStore } from 'mastodon/actions/store';
import { connectUserStream } from 'mastodon/actions/streaming';
import ErrorBoundary from 'mastodon/components/error_boundary';
import { Router } from 'mastodon/components/router';
import UI from 'mastodon/features/ui';
import initialState, { title as siteTitle } from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales';
@ -75,11 +76,11 @@ export default class Mastodon extends PureComponent {
<IntlProvider>
<ReduxProvider store={store}>
<ErrorBoundary>
<BrowserRouter>
<Router>
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
<Route path='/' component={UI} />
</ScrollContext>
</BrowserRouter>
</Router>
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
</ErrorBoundary>

View File

@ -139,7 +139,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(openModal({
modalType: 'EMBED',
modalProps: {
url: status.get('url'),
id: status.get('id'),
onError: error => dispatch(showAlertForError(error)),
},
}));

View File

@ -264,14 +264,14 @@ class Header extends ImmutablePureComponent {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames('logo-button', { 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
actionBtn = <Button className={classNames({ 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']), 'button--with-bell': bellBtn !== '' })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
}
} else {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
}
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {

View File

@ -160,16 +160,16 @@ class AccountCard extends ImmutablePureComponent {
if (!account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
} else if (account.getIn(['relationship', 'muting'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
actionBtn = <Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
} else if (account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
actionBtn = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
}
} else {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
}
return (

View File

@ -25,12 +25,13 @@ export type SearchData = [
BaseEmoji['native'],
Emoji['short_names'],
Search,
Emoji['unified']
Emoji['unified'],
];
export interface ShortCodesToEmojiData {
[key: ShortCodesToEmojiDataKey]: [FilenameData, SearchData];
}
export type ShortCodesToEmojiData = Record<
ShortCodesToEmojiDataKey,
[FilenameData, SearchData]
>;
export type EmojisWithoutShortCodes = FilenameData[];
export type EmojiCompressed = [
@ -38,7 +39,7 @@ export type EmojiCompressed = [
Skins,
Category[],
Data['aliases'],
EmojisWithoutShortCodes
EmojisWithoutShortCodes,
];
/*

View File

@ -9,7 +9,7 @@ import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
type Emojis = {
[key in keyof ShortCodesToEmojiData]: {
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
native: BaseEmoji['native'];
search: Search;
short_names: Emoji['short_names'];

View File

@ -52,6 +52,7 @@ class Statuses extends PureComponent {
<StatusList
trackScroll
timelineId='explore'
statusIds={statusIds}
scrollKey='explore-statuses'
hasMore={hasMore}

View File

@ -142,7 +142,7 @@ class GettingStarted extends ImmutablePureComponent {
{!multiColumn && <div className='flex-spacer' />}
<LinkFooter />
<LinkFooter multiColumn />
</div>
{(multiColumn && showTrends) && <TrendsContainer />}

View File

@ -1,38 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import SettingToggle from '../../notifications/components/setting_toggle';
class ColumnSettings extends PureComponent {
static propTypes = {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
render () {
const { settings, onChange } = this.props;
return (
<div>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
<div className='column-settings__row'>
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show boosts' />} />
</div>
<div className='column-settings__row'>
<SettingToggle prefix='home_timeline' settings={settings} settingPath={['shows', 'reply']} onChange={onChange} label={<FormattedMessage id='home.column_settings.show_replies' defaultMessage='Show replies' />} />
</div>
</div>
);
}
}
export default injectIntl(ColumnSettings);

View File

@ -0,0 +1,66 @@
/* eslint-disable @typescript-eslint/no-unsafe-call,
@typescript-eslint/no-unsafe-return,
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access
-- the settings store is not yet typed */
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { changeSetting } from '../../../actions/settings';
import SettingToggle from '../../notifications/components/setting_toggle';
export const ColumnSettings: React.FC = () => {
const settings = useAppSelector((state) => state.settings.get('home'));
const dispatch = useAppDispatch();
const onChange = useCallback(
(key: string, checked: boolean) => {
dispatch(changeSetting(['home', ...key], checked));
},
[dispatch],
);
return (
<div>
<span className='column-settings__section'>
<FormattedMessage
id='home.column_settings.basic'
defaultMessage='Basic'
/>
</span>
<div className='column-settings__row'>
<SettingToggle
prefix='home_timeline'
settings={settings}
settingPath={['shows', 'reblog']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_reblogs'
defaultMessage='Show boosts'
/>
}
/>
</div>
<div className='column-settings__row'>
<SettingToggle
prefix='home_timeline'
settings={settings}
settingPath={['shows', 'reply']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_replies'
defaultMessage='Show replies'
/>
}
/>
</div>
</div>
);
};

View File

@ -1,25 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import background from 'mastodon/../images/friends-cropped.png';
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
export const ExplorePrompt = () => (
<DismissableBanner id='home.explore_prompt'>
<img src={background} alt='' className='dismissable-banner__background-image' />
<h1><FormattedMessage id='home.explore_prompt.title' defaultMessage='This is your home base within Mastodon.' /></h1>
<p><FormattedMessage id='home.explore_prompt.body' defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" /></p>
<div className='dismissable-banner__message__actions__wrapper'>
<div className='dismissable-banner__message__actions'>
<Link to='/explore' className='button'><FormattedMessage id='home.actions.go_to_explore' defaultMessage="See what's trending" /></Link>
<Link to='/explore/suggestions' className='button button-tertiary'><FormattedMessage id='home.actions.go_to_suggestions' defaultMessage='Find people to follow' /></Link>
</div>
</div>
</DismissableBanner>
);

View File

@ -0,0 +1,46 @@
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import background from 'mastodon/../images/friends-cropped.png';
import { DismissableBanner } from 'mastodon/components/dismissable_banner';
export const ExplorePrompt = () => (
<DismissableBanner id='home.explore_prompt'>
<img
src={background}
alt=''
className='dismissable-banner__background-image'
/>
<h1>
<FormattedMessage
id='home.explore_prompt.title'
defaultMessage='This is your home base within Mastodon.'
/>
</h1>
<p>
<FormattedMessage
id='home.explore_prompt.body'
defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:"
/>
</p>
<div className='dismissable-banner__message__wrapper'>
<div className='dismissable-banner__message__actions'>
<Link to='/explore' className='button'>
<FormattedMessage
id='home.actions.go_to_explore'
defaultMessage="See what's trending"
/>
</Link>
<Link to='/explore/suggestions' className='button button-tertiary'>
<FormattedMessage
id='home.actions.go_to_suggestions'
defaultMessage='Find people to follow'
/>
</Link>
</div>
</div>
</DismissableBanner>
);

View File

@ -1,22 +0,0 @@
import { connect } from 'react-redux';
import { changeSetting, saveSettings } from '../../../actions/settings';
import ColumnSettings from '../components/column_settings';
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'home']),
});
const mapDispatchToProps = dispatch => ({
onChange (key, checked) {
dispatch(changeSetting(['home', ...key], checked));
},
onSave () {
dispatch(saveSettings());
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@ -22,8 +22,8 @@ import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import StatusListContainer from '../ui/containers/status_list_container';
import { ColumnSettings } from './components/column_settings';
import { ExplorePrompt } from './components/explore_prompt';
import ColumnSettingsContainer from './containers/column_settings_container';
const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
@ -191,7 +191,7 @@ class HomeTimeline extends PureComponent {
extraButton={announcementsButton}
appendContent={hasAnnouncements && showAnnouncements && <AnnouncementsContainer />}
>
<ColumnSettingsContainer />
<ColumnSettings />
</ColumnHeader>
{signedIn ? (

View File

@ -1,87 +1,121 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { OrderedSet, List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { shallowEqual } from 'react-redux';
import { createSelector } from 'reselect';
import Toggle from 'react-toggle';
import { fetchAccount } from 'mastodon/actions/accounts';
import Button from 'mastodon/components/button';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
});
class Comment extends PureComponent {
const selectRepliedToAccountIds = createSelector(
[
(state) => state.get('statuses'),
(_, statusIds) => statusIds,
],
(statusesMap, statusIds) => statusIds.map((statusId) => statusesMap.getIn([statusId, 'in_reply_to_account_id'])),
{
resultEqualityCheck: shallowEqual,
}
);
static propTypes = {
onSubmit: PropTypes.func.isRequired,
comment: PropTypes.string.isRequired,
onChangeComment: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
isSubmitting: PropTypes.bool,
forward: PropTypes.bool,
isRemote: PropTypes.bool,
domain: PropTypes.string,
onChangeForward: PropTypes.func.isRequired,
};
const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedDomains, onSubmit, onChangeComment, onToggleDomain }) => {
const intl = useIntl();
handleClick = () => {
const { onSubmit } = this.props;
onSubmit();
};
const dispatch = useAppDispatch();
const loadedRef = useRef(false);
handleChange = e => {
const { onChangeComment } = this.props;
onChangeComment(e.target.value);
};
const handleClick = useCallback(() => onSubmit(), [onSubmit]);
const handleChange = useCallback((e) => onChangeComment(e.target.value), [onChangeComment]);
const handleToggleDomain = useCallback(e => onToggleDomain(e.target.value, e.target.checked), [onToggleDomain]);
handleKeyDown = e => {
const handleKeyDown = useCallback((e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleClick();
handleClick();
}
};
}, [handleClick]);
handleForwardChange = e => {
const { onChangeForward } = this.props;
onChangeForward(e.target.checked);
};
// Memoize accountIds since we don't want it to trigger `useEffect` on each render
const accountIds = useAppSelector((state) => domain ? selectRepliedToAccountIds(state, statusIds) : ImmutableList());
render () {
const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props;
// While we could memoize `availableDomains`, it is pretty inexpensive to recompute
const accountsMap = useAppSelector((state) => state.get('accounts'));
const availableDomains = domain ? OrderedSet([domain]).union(accountIds.map((accountId) => accountsMap.getIn([accountId, 'acct'], '').split('@')[1]).filter(domain => !!domain)) : OrderedSet();
return (
<>
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3>
useEffect(() => {
if (loadedRef.current) {
return;
}
<textarea
className='report-dialog-modal__textarea'
placeholder={intl.formatMessage(messages.placeholder)}
value={comment}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
disabled={isSubmitting}
/>
loadedRef.current = true;
{isRemote && (
<>
<p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
// First, pre-select known domains
availableDomains.forEach((domain) => {
onToggleDomain(domain, true);
});
<label className='report-dialog-modal__toggle'>
<Toggle checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
// Then, fetch missing replied-to accounts
const unknownAccounts = OrderedSet(accountIds.filter(accountId => accountId && !accountsMap.has(accountId)));
unknownAccounts.forEach((accountId) => {
dispatch(fetchAccount(accountId));
});
});
return (
<>
<h3 className='report-dialog-modal__title'><FormattedMessage id='report.comment.title' defaultMessage='Is there anything else you think we should know?' /></h3>
<textarea
className='report-dialog-modal__textarea'
placeholder={intl.formatMessage(messages.placeholder)}
value={comment}
onChange={handleChange}
onKeyDown={handleKeyDown}
disabled={isSubmitting}
/>
{isRemote && (
<>
<p className='report-dialog-modal__lead'><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' /></p>
{ availableDomains.map((domain) => (
<label className='report-dialog-modal__toggle' key={`toggle-${domain}`}>
<Toggle checked={selectedDomains.includes(domain)} disabled={isSubmitting} onChange={handleToggleDomain} value={domain} />
<FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />
</label>
</>
)}
))}
</>
)}
<div className='flex-spacer' />
<div className='report-dialog-modal__actions'>
<Button onClick={this.handleClick} disabled={isSubmitting}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
</div>
</>
);
}
<div className='flex-spacer' />
<div className='report-dialog-modal__actions'>
<Button onClick={handleClick} disabled={isSubmitting}><FormattedMessage id='report.submit' defaultMessage='Submit report' /></Button>
</div>
</>
);
}
export default injectIntl(Comment);
Comment.propTypes = {
comment: PropTypes.string.isRequired,
domain: PropTypes.string,
statusIds: ImmutablePropTypes.list.isRequired,
isRemote: PropTypes.bool,
isSubmitting: PropTypes.bool,
selectedDomains: ImmutablePropTypes.set.isRequired,
onSubmit: PropTypes.func.isRequired,
onChangeComment: PropTypes.func.isRequired,
onToggleDomain: PropTypes.func.isRequired,
};
export default Comment;

View File

@ -205,7 +205,7 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
}
if (publicStatus) {
if (publicStatus && (signedIn || !isRemote)) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}

View File

@ -110,7 +110,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(openModal({
modalType: 'EMBED',
modalProps: {
url: status.get('url'),
id: status.get('id'),
onError: error => dispatch(showAlertForError(error)),
},
}));

View File

@ -449,7 +449,7 @@ class Status extends ImmutablePureComponent {
handleEmbed = (status) => {
this.props.dispatch(openModal({
modalType: 'EMBED',
modalProps: { url: status.get('url') },
modalProps: { id: status.get('id') },
}));
};

View File

@ -14,7 +14,7 @@ const messages = defineMessages({
class EmbedModal extends ImmutablePureComponent {
static propTypes = {
url: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onError: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
@ -26,11 +26,11 @@ class EmbedModal extends ImmutablePureComponent {
};
componentDidMount () {
const { url } = this.props;
const { id } = this.props;
this.setState({ loading: true });
api().post('/api/web/embed', { url }).then(res => {
api().get(`/api/web/embeds/${id}`).then(res => {
this.setState({ loading: false, oembed: res.data });
const iframeDocument = this.iframe.contentWindow.document;

View File

@ -38,6 +38,7 @@ class LinkFooter extends PureComponent {
};
static propTypes = {
multiColumn: PropTypes.bool,
onLogout: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
@ -53,6 +54,7 @@ class LinkFooter extends PureComponent {
render () {
const { signedIn, permissions } = this.context.identity;
const { multiColumn } = this.props;
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
const canProfileDirectory = profileDirectory;
@ -64,7 +66,7 @@ class LinkFooter extends PureComponent {
<p>
<strong>{domain}</strong>:
{' '}
<Link to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
<Link to='/about' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
{statusPageUrl && (
<>
{DividingCircle}
@ -84,7 +86,7 @@ class LinkFooter extends PureComponent {
</>
)}
{DividingCircle}
<Link to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
<Link to='/privacy-policy' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
</p>
<p>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { WordmarkLogo } from 'mastodon/components/logo';
import NavigationPortal from 'mastodon/components/navigation_portal';
import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
import { transientSingleColumn } from 'mastodon/is_mobile';
import ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner';
@ -29,6 +30,7 @@ const messages = defineMessages({
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
});
class NavigationPanel extends Component {
@ -54,6 +56,12 @@ class NavigationPanel extends Component {
<div className='navigation-panel'>
<div className='navigation-panel__logo'>
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
{transientSingleColumn && (
<a href={`/deck${location.pathname}`} className='button button--block'>
{intl.formatMessage(messages.advancedInterface)}
</a>
)}
<hr />
</div>

View File

@ -45,25 +45,26 @@ class ReportModal extends ImmutablePureComponent {
state = {
step: 'category',
selectedStatusIds: OrderedSet(this.props.statusId ? [this.props.statusId] : []),
selectedDomains: OrderedSet(),
comment: '',
category: null,
selectedRuleIds: OrderedSet(),
forward: true,
isSubmitting: false,
isSubmitted: false,
};
handleSubmit = () => {
const { dispatch, accountId } = this.props;
const { selectedStatusIds, comment, category, selectedRuleIds, forward } = this.state;
const { selectedStatusIds, selectedDomains, comment, category, selectedRuleIds } = this.state;
this.setState({ isSubmitting: true });
dispatch(submitReport({
account_id: accountId,
status_ids: selectedStatusIds.toArray(),
selected_domains: selectedDomains.toArray(),
comment,
forward,
forward: selectedDomains.size > 0,
category,
rule_ids: selectedRuleIds.toArray(),
}, this.handleSuccess, this.handleFail));
@ -87,13 +88,19 @@ class ReportModal extends ImmutablePureComponent {
}
};
handleRuleToggle = (ruleId, checked) => {
const { selectedRuleIds } = this.state;
handleDomainToggle = (domain, checked) => {
if (checked) {
this.setState({ selectedRuleIds: selectedRuleIds.add(ruleId) });
this.setState((state) => ({ selectedDomains: state.selectedDomains.add(domain) }));
} else {
this.setState({ selectedRuleIds: selectedRuleIds.remove(ruleId) });
this.setState((state) => ({ selectedDomains: state.selectedDomains.remove(domain) }));
}
};
handleRuleToggle = (ruleId, checked) => {
if (checked) {
this.setState((state) => ({ selectedRuleIds: state.selectedRuleIds.add(ruleId) }));
} else {
this.setState((state) => ({ selectedRuleIds: state.selectedRuleIds.remove(ruleId) }));
}
};
@ -105,10 +112,6 @@ class ReportModal extends ImmutablePureComponent {
this.setState({ comment });
};
handleChangeForward = forward => {
this.setState({ forward });
};
handleNextStep = step => {
this.setState({ step });
};
@ -136,8 +139,8 @@ class ReportModal extends ImmutablePureComponent {
step,
selectedStatusIds,
selectedRuleIds,
selectedDomains,
comment,
forward,
category,
isSubmitting,
isSubmitted,
@ -185,10 +188,11 @@ class ReportModal extends ImmutablePureComponent {
isSubmitting={isSubmitting}
isRemote={isRemote}
comment={comment}
forward={forward}
domain={domain}
onChangeComment={this.handleChangeComment}
onChangeForward={this.handleChangeForward}
statusIds={selectedStatusIds}
selectedDomains={selectedDomains}
onToggleDomain={this.handleDomainToggle}
/>
);
break;

View File

@ -126,11 +126,11 @@ class SwitchingColumnsArea extends PureComponent {
static propTypes = {
children: PropTypes.node,
location: PropTypes.object,
mobile: PropTypes.bool,
singleColumn: PropTypes.bool,
};
UNSAFE_componentWillMount () {
if (this.props.mobile) {
if (this.props.singleColumn) {
document.body.classList.toggle('layout-single-column', true);
document.body.classList.toggle('layout-multiple-columns', false);
} else {
@ -144,9 +144,9 @@ class SwitchingColumnsArea extends PureComponent {
this.node.handleChildrenContentChange();
}
if (prevProps.mobile !== this.props.mobile) {
document.body.classList.toggle('layout-single-column', this.props.mobile);
document.body.classList.toggle('layout-multiple-columns', !this.props.mobile);
if (prevProps.singleColumn !== this.props.singleColumn) {
document.body.classList.toggle('layout-single-column', this.props.singleColumn);
document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn);
}
}
@ -157,16 +157,17 @@ class SwitchingColumnsArea extends PureComponent {
};
render () {
const { children, mobile } = this.props;
const { children, singleColumn } = this.props;
const { signedIn } = this.context.identity;
const pathName = this.props.location.pathname;
let redirect;
if (signedIn) {
if (mobile) {
if (singleColumn) {
redirect = <Redirect from='/' to='/home' exact />;
} else {
redirect = <Redirect from='/' to='/getting-started' exact />;
redirect = <Redirect from='/' to='/deck/getting-started' exact />;
}
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
@ -177,10 +178,13 @@ class SwitchingColumnsArea extends PureComponent {
}
return (
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
<WrappedSwitch>
{redirect}
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
<WrappedRoute path='/about' component={About} content={children} />
@ -573,7 +577,7 @@ class UI extends PureComponent {
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
<Header />
<SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'}>
<SwitchingColumnsArea location={location} singleColumn={layout === 'mobile' || layout === 'single-column'}>
{children}
</SwitchingColumnsArea>

View File

@ -11,13 +11,21 @@ import BundleContainer from '../containers/bundle_container';
// Small wrapper to pass multiColumn to the route components
export class WrappedSwitch extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
render () {
const { multiColumn, children } = this.props;
const { location } = this.context.router.route;
const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
? {...location, pathname: location.pathname.slice(5)}
: location;
return (
<Switch>
{Children.map(children, child => cloneElement(child, { multiColumn }))}
<Switch location={decklessLocation}>
{Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
</Switch>
);
}

View File

@ -94,6 +94,13 @@ const element = document.getElementById('initial-state');
/** @type {InitialState | undefined} */
const initialState = element?.textContent && JSON.parse(element.textContent);
/** @type {string} */
const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? '';
/** @type {boolean} */
export const hasMultiColumnPath = initialPath === '/'
|| initialPath === '/getting-started'
|| initialPath.startsWith('/deck');
/**
* @template {keyof InitialStateMeta} K
* @param {K} prop

View File

@ -1,19 +1,21 @@
import { supportsPassiveEvents } from 'detect-passive-events';
import { forceSingleColumn } from './initial_state';
import { forceSingleColumn, hasMultiColumnPath } from './initial_state';
const LAYOUT_BREAKPOINT = 630;
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
export const transientSingleColumn = !forceSingleColumn && !hasMultiColumnPath;
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
export const layoutFromWindow = (): LayoutType => {
if (isMobile(window.innerWidth)) {
return 'mobile';
} else if (forceSingleColumn) {
return 'single-column';
} else {
} else if (!forceSingleColumn && !transientSingleColumn) {
return 'multi-column';
} else {
return 'single-column';
}
};

View File

@ -385,6 +385,7 @@
"mute_modal.hide_notifications": "Hide notifications from this user?",
"mute_modal.indefinite": "Indefinite",
"navigation_bar.about": "About",
"navigation_bar.advanced_interface": "Open in advanced web interface",
"navigation_bar.blocks": "Blocked users",
"navigation_bar.bookmarks": "Bookmarks",
"navigation_bar.community_timeline": "Local timeline",

View File

@ -368,6 +368,7 @@
"mute_modal.hide_notifications": "Masquer les notifications de cette personne?",
"mute_modal.indefinite": "Indéfinie",
"navigation_bar.about": "À propos",
"navigation_bar.advanced_interface": "Ouvrir dans linterface avancée",
"navigation_bar.blocks": "Comptes bloqués",
"navigation_bar.bookmarks": "Marque-pages",
"navigation_bar.community_timeline": "Fil public local",

View File

@ -3,15 +3,19 @@ export interface LocaleData {
messages: Record<string, string>;
}
let loadedLocale: LocaleData;
let loadedLocale: LocaleData | undefined;
export function setLocale(locale: LocaleData) {
loadedLocale = locale;
}
export function getLocale() {
if (!loadedLocale && process.env.NODE_ENV === 'development') {
throw new Error('getLocale() called before any locale has been set');
export function getLocale(): LocaleData {
if (!loadedLocale) {
if (process.env.NODE_ENV === 'development') {
throw new Error('getLocale() called before any locale has been set');
} else {
return { locale: 'unknown', messages: {} };
}
}
return loadedLocale;

View File

@ -6,6 +6,7 @@ import { isLocaleLoaded, setLocale } from './global_locale';
const localeLoadingSemaphore = new Semaphore(1);
export async function loadLocale() {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
const locale = document.querySelector<HTMLElement>('html')?.lang || 'en';
// We use a Semaphore here so only one thing can try to load the locales at

View File

@ -4,7 +4,7 @@ import 'core-js/features/symbol';
import 'core-js/features/promise/finally';
import { decode as decodeBase64 } from '../utils/base64';
if (!HTMLCanvasElement.prototype.toBlob) {
if (!Object.hasOwn(HTMLCanvasElement.prototype, 'toBlob')) {
const BASE64_MARKER = ';base64,';
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
@ -12,12 +12,12 @@ if (!HTMLCanvasElement.prototype.toBlob) {
this: HTMLCanvasElement,
callback: BlobCallback,
type = 'image/png',
quality: unknown
quality: unknown,
) {
const dataURL: string = this.toDataURL(type, quality);
let data;
if (dataURL.indexOf(BASE64_MARKER) >= 0) {
if (dataURL.includes(BASE64_MARKER)) {
const [, base64] = dataURL.split(BASE64_MARKER);
data = decodeBase64(base64);
} else {

View File

@ -24,6 +24,7 @@ export function loadPolyfills() {
// Latest version of Firefox and Safari do not have IntersectionObserver.
// Edge does not have requestIdleCallback.
// This avoids shipping them all the polyfills.
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types */
const needsExtraPolyfills = !(
window.AbortController &&
window.IntersectionObserver &&
@ -31,6 +32,7 @@ export function loadPolyfills() {
'isIntersecting' in IntersectionObserverEntry.prototype &&
window.requestIdleCallback
);
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
return Promise.all([
loadIntlPolyfills(),

View File

@ -80,6 +80,7 @@ async function loadIntlPluralRulesPolyfills(locale: string) {
// }
export async function loadIntlPolyfills() {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
const locale = document.querySelector('html')?.lang || 'en';
// order is important here

View File

@ -99,7 +99,7 @@ const initialRootState = Object.fromEntries(
reducer(undefined, {
// empty action
}),
])
]),
);
const RootStateRecord = ImmutableRecord(initialRootState, 'RootState');

View File

@ -35,7 +35,7 @@ interface PopModalOption {
}
const popModal = (
state: State,
{ modalType, ignoreFocus }: PopModalOption
{ modalType, ignoreFocus }: PopModalOption,
): State => {
if (
modalType === undefined ||
@ -52,12 +52,12 @@ const popModal = (
const pushModal = (
state: State,
modalType: ModalType,
modalProps: ModalProps
modalProps: ModalProps,
): State => {
return state.withMutations((record) => {
record.set('ignoreFocus', false);
record.update('stack', (stack) =>
stack.unshift(Modal({ modalType, modalProps }))
stack.unshift(Modal({ modalType, modalProps })),
);
});
};
@ -68,14 +68,14 @@ export function modalReducer(
modalType: ModalType;
ignoreFocus: boolean;
modalProps: Record<string, unknown>;
}>
}>,
) {
switch (action.type) {
case openModal.type:
return pushModal(
state,
action.payload.modalType,
action.payload.modalProps
action.payload.modalProps,
);
case closeModal.type:
return popModal(state, action.payload);
@ -85,8 +85,8 @@ export function modalReducer(
return state.update('stack', (stack) =>
stack.filterNot(
// @ts-expect-error TIMELINE_DELETE action is not typed yet.
(modal) => modal.get('modalProps').statusId === action.id
)
(modal) => modal.get('modalProps').statusId === action.id,
),
);
default:
return state;

View File

@ -3,12 +3,12 @@ const easingOutQuint = (
t: number,
b: number,
c: number,
d: number
d: number,
) => c * ((t = t / d - 1) * t * t * t * t + 1) + b;
const scroll = (
node: Element,
key: 'scrollTop' | 'scrollLeft',
target: number
target: number,
) => {
const startTime = Date.now();
const offset = node[key];
@ -38,11 +38,13 @@ const scroll = (
const isScrollBehaviorSupported =
'scrollBehavior' in document.documentElement.style;
export const scrollRight = (node: Element, position: number) =>
isScrollBehaviorSupported
? node.scrollTo({ left: position, behavior: 'smooth' })
: scroll(node, 'scrollLeft', position);
export const scrollTop = (node: Element) =>
isScrollBehaviorSupported
? node.scrollTo({ top: 0, behavior: 'smooth' })
: scroll(node, 'scrollTop', 0);
export const scrollRight = (node: Element, position: number) => {
if (isScrollBehaviorSupported)
node.scrollTo({ left: position, behavior: 'smooth' });
else scroll(node, 'scrollLeft', position);
};
export const scrollTop = (node: Element) => {
if (isScrollBehaviorSupported) node.scrollTo({ top: 0, behavior: 'smooth' });
else scroll(node, 'scrollTop', 0);
};

View File

@ -30,7 +30,7 @@ export const store = configureStore({
.concat(
loadingBarMiddleware({
promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'],
})
}),
)
.concat(errorsMiddleware)
.concat(soundsMiddleware()),

View File

@ -14,9 +14,9 @@ const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [
];
export const loadingBarMiddleware = (
config: Config = {}
config: Config = {},
): Middleware<Record<string, never>, RootState> => {
const promiseTypeSuffixes = config.promiseTypeSuffixes || defaultTypeSuffixes;
const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes;
return ({ dispatch }) =>
(next) =>
@ -32,7 +32,7 @@ export const loadingBarMiddleware = (
if (action.type.match(isPending)) {
dispatch(showLoading());
} else if (
action.type.match(isFulfilled) ||
action.type.match(isFulfilled) ??
action.type.match(isRejected)
) {
dispatch(hideLoading());

View File

@ -1,5 +1,8 @@
import type { Middleware, AnyAction } from 'redux';
import ready from 'mastodon/ready';
import { assetHost } from 'mastodon/utils/config';
import type { RootState } from '..';
interface AudioSource {
@ -35,25 +38,27 @@ export const soundsMiddleware = (): Middleware<
Record<string, never>,
RootState
> => {
const soundCache: { [key: string]: HTMLAudioElement } = {
boop: createAudio([
const soundCache: Record<string, HTMLAudioElement> = {};
void ready(() => {
soundCache.boop = createAudio([
{
src: '/sounds/boop.ogg',
src: `${assetHost}/sounds/boop.ogg`,
type: 'audio/ogg',
},
{
src: '/sounds/boop.mp3',
src: `${assetHost}/sounds/boop.mp3`,
type: 'audio/mpeg',
},
]),
};
]);
});
return () =>
(next) =>
(action: AnyAction & { meta?: { sound?: string } }) => {
const sound = action?.meta?.sound;
const sound = action.meta?.sound;
if (sound && soundCache[sound]) {
if (sound && Object.hasOwn(soundCache, sound)) {
play(soundCache[sound]);
}

View File

@ -7,7 +7,7 @@ export const toServerSideType = (columnType: string) => {
case 'account':
return columnType;
default:
if (columnType.indexOf('list:') > -1) {
if (columnType.includes('list:')) {
return 'home';
} else {
return 'public'; // community, account, hashtag

View File

@ -6,7 +6,7 @@ const buildHashtagPatternRegex = () => {
try {
return new RegExp(
`(?:^|[^\\/\\)\\w])#(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))`,
'iu'
'iu',
);
} catch {
return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
@ -17,7 +17,7 @@ const buildHashtagRegex = () => {
try {
return new RegExp(
`^(([${WORD}_][${WORD}${HASHTAG_SEPARATORS}]*[${ALPHA}${HASHTAG_SEPARATORS}][${WORD}${HASHTAG_SEPARATORS}]*[${WORD}_])|([${WORD}_]*[${ALPHA}][${WORD}_]*))$`,
'iu'
'iu',
);
} catch {
return /^(\w*[a-zA-Z·]\w*)$/i;

View File

@ -55,7 +55,7 @@ export function toShortNumber(sourceNumber: number): ShortNumber {
*/
export function pluralReady(
sourceNumber: number,
division: DecimalUnits
division: DecimalUnits | null,
): number {
if (division == null || division < DECIMAL_UNITS.HUNDRED) {
return sourceNumber;

View File

@ -4,6 +4,5 @@ export function uuid(a?: string): string {
(a as unknown as number) ^
((Math.random() * 16) >> ((a as unknown as number) / 4))
).toString(16)
: // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
: ('' + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
}

View File

@ -1,6 +1,7 @@
@font-face {
font-family: mastodon-font-monospace;
src: local('Roboto Mono'),
src:
local('Roboto Mono'),
url('../fonts/roboto-mono/robotomono-regular-webfont.woff2') format('woff2'),
url('../fonts/roboto-mono/robotomono-regular-webfont.woff') format('woff'),
url('../fonts/roboto-mono/robotomono-regular-webfont.ttf')

View File

@ -1,6 +1,7 @@
@font-face {
font-family: mastodon-font-sans-serif;
src: local('Roboto Italic'),
src:
local('Roboto Italic'),
url('../fonts/roboto/roboto-italic-webfont.woff2') format('woff2'),
url('../fonts/roboto/roboto-italic-webfont.woff') format('woff'),
url('../fonts/roboto/roboto-italic-webfont.ttf') format('truetype'),
@ -13,7 +14,8 @@
@font-face {
font-family: mastodon-font-sans-serif;
src: local('Roboto Bold'),
src:
local('Roboto Bold'),
url('../fonts/roboto/roboto-bold-webfont.woff2') format('woff2'),
url('../fonts/roboto/roboto-bold-webfont.woff') format('woff'),
url('../fonts/roboto/roboto-bold-webfont.ttf') format('truetype'),
@ -26,7 +28,8 @@
@font-face {
font-family: mastodon-font-sans-serif;
src: local('Roboto Medium'),
src:
local('Roboto Medium'),
url('../fonts/roboto/roboto-medium-webfont.woff2') format('woff2'),
url('../fonts/roboto/roboto-medium-webfont.woff') format('woff'),
url('../fonts/roboto/roboto-medium-webfont.ttf') format('truetype'),
@ -39,7 +42,8 @@
@font-face {
font-family: mastodon-font-sans-serif;
src: local('Roboto'),
src:
local('Roboto'),
url('../fonts/roboto/roboto-regular-webfont.woff2') format('woff2'),
url('../fonts/roboto/roboto-regular-webfont.woff') format('woff'),
url('../fonts/roboto/roboto-regular-webfont.ttf') format('truetype'),

View File

@ -541,7 +541,7 @@ ul.rules-list {
padding-top: 0;
}
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) {
@media only screen and (device-width >= 768px) and (device-width <= 1024px) and (orientation: landscape) {
body {
min-height: 1024px !important;
}

View File

@ -627,14 +627,6 @@ html {
}
}
.button.logo-button {
color: $white;
svg {
fill: $white;
}
}
.notification__filter-bar button.active::after,
.account__section-headline a.active::after {
border-color: transparent transparent $white;

View File

@ -31,9 +31,19 @@ body {
// Droid Sans => Older Androids (<4.0)
// Helvetica Neue => Older macOS <10.11
// $font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0)
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
$font-sans-serif, sans-serif;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
$font-sans-serif,
sans-serif;
}
&.app-body {

View File

@ -747,7 +747,9 @@ body > [data-popper-placement] {
}
.no-reduce-motion .spoiler-input {
transition: height 0.4s ease, opacity 0.4s ease;
transition:
height 0.4s ease,
opacity 0.4s ease;
}
.sign-in-banner {
@ -1421,6 +1423,7 @@ body > [data-popper-placement] {
.detailed-status__link {
color: inherit;
text-decoration: none;
white-space: nowrap;
}
.detailed-status__favorites,
@ -1670,10 +1673,6 @@ a.account__display-name {
color: inherit;
}
.detailed-status .button.logo-button {
margin-bottom: 15px;
}
.detailed-status__display-name {
color: $darker-text-color;
display: flex;
@ -3958,7 +3957,9 @@ a.status-card.compact:hover {
overflow-y: auto;
border-bottom: 1px solid lighten($ui-base-color, 8%);
color: $darker-text-color;
transition: max-height 150ms ease-in-out, opacity 300ms linear;
transition:
max-height 150ms ease-in-out,
opacity 300ms linear;
opacity: 1;
z-index: 1;
position: relative;
@ -5784,6 +5785,7 @@ a.status-card.compact:hover {
&__toggle {
display: flex;
align-items: center;
margin-bottom: 10px;
& > span {
font-size: 17px;
@ -6938,7 +6940,8 @@ noscript {
.navigation-bar {
& > a:first-child {
will-change: margin-top, margin-inline-start, margin-inline-end, width;
transition: margin-top $duration $delay,
transition:
margin-top $duration $delay,
margin-inline-start $duration ($duration + $delay),
margin-inline-end $duration ($duration + $delay);
}
@ -6951,12 +6954,15 @@ noscript {
.navigation-bar__actions {
& > .icon-button.close {
will-change: opacity transform;
transition: opacity $duration * 0.5 $delay, transform $duration $delay;
transition:
opacity $duration * 0.5 $delay,
transform $duration $delay;
}
& > .compose__action-bar .icon-button {
will-change: opacity transform;
transition: opacity $duration * 0.5 $delay + $duration * 0.5,
transition:
opacity $duration * 0.5 $delay + $duration * 0.5,
transform $duration $delay;
}
}
@ -9092,12 +9098,13 @@ noscript {
width: auto;
padding: 15px;
margin: 0;
color: $primary-text-color;
color: $white;
background: rgba($black, 0.85);
backdrop-filter: blur(8px);
border: 1px solid rgba(lighten($ui-base-color, 4%), 0.85);
border: 1px solid rgba(lighten($classic-base-color, 4%), 0.85);
border-radius: 8px;
box-shadow: 0 10px 15px -3px rgba($base-shadow-color, 0.25),
box-shadow:
0 10px 15px -3px rgba($base-shadow-color, 0.25),
0 4px 6px -4px rgba($base-shadow-color, 0.25);
cursor: default;
transition: 0.5s cubic-bezier(0.89, 0.01, 0.5, 1.1);
@ -9123,7 +9130,7 @@ noscript {
text-transform: uppercase;
margin-inline-start: 10px;
cursor: pointer;
color: $highlight-text-color;
color: $blurple-300;
border-radius: 4px;
padding: 0 4px;

View File

@ -77,66 +77,18 @@
}
}
.button.logo-button {
flex: 0 auto;
font-size: 14px;
background: darken($ui-highlight-color, 2%);
color: $primary-text-color;
text-transform: none;
line-height: 1.2;
.button.logo-button svg {
width: 20px;
height: auto;
min-height: 36px;
min-width: 88px;
white-space: normal;
overflow-wrap: break-word;
hyphens: auto;
padding: 0 15px;
border: 0;
svg {
width: 20px;
height: auto;
vertical-align: middle;
margin-inline-end: 5px;
fill: $primary-text-color;
}
&:active,
&:focus,
&:hover {
background: $ui-highlight-color;
}
&:disabled,
&.disabled {
&:active,
&:focus,
&:hover {
background: $ui-primary-color;
}
}
&.button--destructive {
&:active,
&:focus,
&:hover {
background: $error-red;
}
}
vertical-align: middle;
margin-inline-end: 5px;
fill: $primary-text-color;
@media screen and (max-width: $no-gap-breakpoint) {
svg {
display: none;
}
display: none;
}
}
a.button.logo-button {
display: inline-flex;
align-items: center;
justify-content: center;
}
.embed {
.status__content[data-spoiler='folded'] {
.e-content {

View File

@ -143,11 +143,11 @@ class ActivityPub::Activity
end
def follow_request_from_object
@follow_request ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
@follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end
def follow_from_object
@follow ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
@follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end
def fetch_remote_original_status

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'connection_pool'
require_relative './shared_timed_stack'
require_relative 'shared_timed_stack'
class ConnectionPool::SharedConnectionPool < ConnectionPool
def initialize(options = {}, &block)

View File

@ -53,7 +53,7 @@ class EmojiFormatter
end
end
result << Nokogiri::XML::Text.new(text[last_index..-1], tree.document)
result << Nokogiri::XML::Text.new(text[last_index..], tree.document)
node.replace(result)
end

View File

@ -37,7 +37,7 @@ class InlineRenderer
private
def preload_associations_for_status
ActiveRecord::Associations::Preloader.new.preload(@object, {
ActiveRecord::Associations::Preloader.new(records: @object, associations: {
active_mentions: :account,
reblog: {

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require_relative './connection_pool/shared_connection_pool'
require_relative 'connection_pool/shared_connection_pool'
class RequestPool
def self.current

View File

@ -16,7 +16,7 @@ class RSS::Channel < RSS::Element
end
def last_build_date(date)
append_element('lastBuildDate', date.to_formatted_s(:rfc822))
append_element('lastBuildDate', date.to_fs(:rfc822))
end
def image(url, title, link)

View File

@ -20,7 +20,7 @@ class RSS::Item < RSS::Element
end
def pub_date(date)
append_element('pubDate', date.to_formatted_s(:rfc822))
append_element('pubDate', date.to_fs(:rfc822))
end
def description(str)

View File

@ -57,8 +57,8 @@ class TextFormatter
prefix = url.match(URL_PREFIX_REGEX).to_s
display_url = url[prefix.length, 30]
suffix = url[prefix.length + 30..-1]
cutoff = url[prefix.length..-1].length > 30
suffix = url[prefix.length + 30..]
cutoff = url[prefix.length..].length > 30
<<~HTML.squish.html_safe # rubocop:disable Rails/OutputSafety
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}" translate="no"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
@ -84,7 +84,7 @@ class TextFormatter
indices.last
end
result << h(text[last_index..-1])
result << h(text[last_index..])
result
end

View File

@ -1,83 +1,76 @@
# frozen_string_literal: true
class NotificationMailer < ApplicationMailer
helper :accounts
helper :statuses
helper :accounts,
:statuses,
:routing
helper RoutingHelper
before_action :process_params
before_action :set_status, only: [:mention, :favourite, :reblog]
before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request]
def mention(recipient, notification)
@me = recipient
@user = recipient.user
@type = 'mention'
@status = notification.target_status
default to: -> { email_address_with_name(@user.email, @me.username) }
def mention
return unless @user.functional? && @status.present?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
mail subject: default_i18n_subject(name: @status.account.acct)
end
end
def follow(recipient, notification)
@me = recipient
@user = recipient.user
@type = 'follow'
@account = notification.from_account
def follow
return unless @user.functional?
locale_for_account(@me) do
mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
mail subject: default_i18n_subject(name: @account.acct)
end
end
def favourite(recipient, notification)
@me = recipient
@user = recipient.user
@type = 'favourite'
@account = notification.from_account
@status = notification.target_status
def favourite
return unless @user.functional? && @status.present?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
mail subject: default_i18n_subject(name: @account.acct)
end
end
def reblog(recipient, notification)
@me = recipient
@user = recipient.user
@type = 'reblog'
@account = notification.from_account
@status = notification.target_status
def reblog
return unless @user.functional? && @status.present?
locale_for_account(@me) do
thread_by_conversation(@status.conversation)
mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
mail subject: default_i18n_subject(name: @account.acct)
end
end
def follow_request(recipient, notification)
@me = recipient
@user = recipient.user
@type = 'follow_request'
@account = notification.from_account
def follow_request
return unless @user.functional?
locale_for_account(@me) do
mail to: email_address_with_name(@user.email, @me.username), subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
mail subject: default_i18n_subject(name: @account.acct)
end
end
private
def process_params
@notification = params[:notification]
@me = params[:recipient]
@user = @me.user
@type = action_name
end
def set_status
@status = @notification.target_status
end
def set_account
@account = @notification.from_account
end
def thread_by_conversation(conversation)
return if conversation.nil?

View File

@ -25,7 +25,7 @@ class AccountAlias < ApplicationRecord
def acct=(val)
val = val.to_s.strip
super(val.start_with?('@') ? val[1..-1] : val)
super(val.start_with?('@') ? val[1..] : val)
end
def pretty_acct

View File

@ -80,7 +80,7 @@ class Announcement < ApplicationRecord
end
end
ActiveRecord::Associations::Preloader.new.preload(records, :custom_emoji)
ActiveRecord::Associations::Preloader.new(records: records, associations: :custom_emoji)
records
end

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