1
0
Fork 0
forked from gitea/nas

Merge commit 'e54e920e06' into kb_development

This commit is contained in:
KMY 2023-07-14 18:48:24 +09:00
commit 2d1415808b
1422 changed files with 24329 additions and 28786 deletions

View file

@ -1,5 +1,5 @@
# For details, see https://github.com/devcontainers/images/tree/main/src/ruby # For details, see https://github.com/devcontainers/images/tree/main/src/ruby
FROM mcr.microsoft.com/devcontainers/ruby:0-3.2-bullseye FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
# Install Rails # Install Rails
# RUN gem install rails webdrivers # RUN gem install rails webdrivers

View file

@ -69,7 +69,7 @@ services:
hard: -1 hard: -1
libretranslate: libretranslate:
image: libretranslate/libretranslate:v1.3.10 image: libretranslate/libretranslate:v1.3.11
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- lt-data:/home/libretranslate/.local - lt-data:/home/libretranslate/.local

View file

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

View file

@ -15,7 +15,6 @@
// Ignore major version bumps for these node packages // Ignore major version bumps for these node packages
matchManagers: ['npm'], matchManagers: ['npm'],
matchPackageNames: [ matchPackageNames: [
'@rails/ujs', // Needs to match the major Rails version
'tesseract.js', // Requires code changes 'tesseract.js', // Requires code changes
'react-hotkeys', // Requires code changes 'react-hotkeys', // Requires code changes
@ -51,12 +50,6 @@
'sidekiq', // Requires manual upgrade 'sidekiq', // Requires manual upgrade
'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version 'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
'redis', // Requires manual upgrade 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'], matchUpdateTypes: ['major'],
enabled: false, enabled: false,

View file

@ -52,7 +52,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch # Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release # This needs to be updated after each minor version release
flavor: | flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') && 'auto' || 'false' }} latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
tags: | tags: |
type=edge,branch=main type=edge,branch=main
type=pep440,pattern={{raw}} type=pep440,pattern={{raw}}

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

View file

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

View file

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

View file

@ -24,7 +24,6 @@ AllCops:
Exclude: Exclude:
- 'db/schema.rb' - 'db/schema.rb'
- 'bin/*' - 'bin/*'
- 'Rakefile'
- 'node_modules/**/*' - 'node_modules/**/*'
- 'Vagrantfile' - 'Vagrantfile'
- 'vendor/**/*' - 'vendor/**/*'
@ -202,6 +201,11 @@ Style/RedundantBegin:
Style/RescueStandardError: Style/RescueStandardError:
EnforcedStyle: implicit 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 # Reason: Originally disabled for CodeClimate, and no config consensus has been found
# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray # https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray
Style/SymbolArray: Style/SymbolArray:

View file

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` # `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.50.2. # using RuboCop version 1.54.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -28,7 +28,6 @@ Layout/ArgumentAlignment:
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
Layout/HashAlignment: Layout/HashAlignment:
Exclude: Exclude:
- 'config/boot.rb'
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/rack_attack.rb' - 'config/initializers/rack_attack.rb'
- 'config/routes.rb' - 'config/routes.rb'
@ -48,19 +47,6 @@ Layout/SpaceInLambdaLiteral:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb' - 'config/initializers/content_security_policy.rb'
# Configuration parameters: AllowedMethods, AllowedPatterns.
Lint/AmbiguousBlockAssociation:
Exclude:
- 'spec/controllers/admin/account_moderation_notes_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb'
- 'spec/services/activitypub/process_status_update_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
- 'spec/services/suspend_account_service_spec.rb'
- 'spec/services/unsuspend_account_service_spec.rb'
- 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
# Configuration parameters: AllowComments, AllowEmptyLambdas. # Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock: Lint/EmptyBlock:
Exclude: Exclude:
@ -110,11 +96,6 @@ Lint/OrAssignmentToConstant:
Exclude: Exclude:
- 'lib/sanitize_ext/sanitize_config.rb' - 'lib/sanitize_ext/sanitize_config.rb'
# This cop supports safe autocorrection (--autocorrect).
Lint/SendWithMixinArgument:
Exclude:
- 'config/application.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument: Lint/UnusedBlockArgument:
@ -124,6 +105,7 @@ Lint/UnusedBlockArgument:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/initializers/simple_form.rb' - 'config/initializers/simple_form.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/UselessAssignment: Lint/UselessAssignment:
Exclude: Exclude:
- 'app/services/activitypub/process_status_update_service.rb' - 'app/services/activitypub/process_status_update_service.rb'
@ -145,6 +127,7 @@ Lint/UselessAssignment:
- 'spec/services/resolve_url_service_spec.rb' - 'spec/services/resolve_url_service_spec.rb'
- 'spec/views/statuses/show.html.haml_spec.rb' - 'spec/views/statuses/show.html.haml_spec.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: CheckForMethodsWithNoSideEffects. # Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void: Lint/Void:
Exclude: Exclude:
@ -165,11 +148,7 @@ Metrics/CyclomaticComplexity:
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 28 Max: 27
Naming/AccessorMethodName:
Exclude:
- 'app/controllers/auth/sessions_controller.rb'
# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms.
# CheckDefinitionPathHierarchyRoots: lib, spec, test, src # CheckDefinitionPathHierarchyRoots: lib, spec, test, src
@ -178,18 +157,6 @@ Naming/FileName:
Exclude: Exclude:
- 'config/locales/sr-Latn.rb' - 'config/locales/sr-Latn.rb'
# 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. # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
# SupportedStyles: snake_case, normalcase, non_integer # SupportedStyles: snake_case, normalcase, non_integer
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
@ -200,14 +167,9 @@ Naming/VariableNumber:
- 'db/migrate/20190820003045_update_statuses_index.rb' - 'db/migrate/20190820003045_update_statuses_index.rb'
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb' - 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb' - 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
- 'spec/controllers/activitypub/followers_synchronizations_controller_spec.rb'
- 'spec/lib/feed_manager_spec.rb'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
- 'spec/models/concerns/account_interactions_spec.rb'
- 'spec/models/custom_emoji_filter_spec.rb'
- 'spec/models/domain_block_spec.rb' - 'spec/models/domain_block_spec.rb'
- 'spec/models/user_spec.rb' - 'spec/models/user_spec.rb'
- 'spec/services/activitypub/fetch_featured_collection_service_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Performance/UnfreezeString: Performance/UnfreezeString:
@ -289,7 +251,6 @@ RSpec/HookArgument:
- 'spec/serializers/activitypub/note_serializer_spec.rb' - 'spec/serializers/activitypub/note_serializer_spec.rb'
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb' - 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
- 'spec/services/import_service_spec.rb' - 'spec/services/import_service_spec.rb'
- 'spec/spec_helper.rb'
# Configuration parameters: AssignmentOnly. # Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable: RSpec/InstanceVariable:
@ -320,11 +281,8 @@ RSpec/LetSetup:
- 'spec/controllers/admin/statuses_controller_spec.rb' - 'spec/controllers/admin/statuses_controller_spec.rb'
- 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb'
- 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb'
- 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb'
- 'spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb'
- 'spec/controllers/api/v1/filters_controller_spec.rb' - 'spec/controllers/api/v1/filters_controller_spec.rb'
- 'spec/controllers/api/v1/followed_tags_controller_spec.rb' - 'spec/controllers/api/v1/followed_tags_controller_spec.rb'
- 'spec/controllers/api/v1/tags_controller_spec.rb'
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
- 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb'
- 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb'
@ -361,7 +319,6 @@ RSpec/LetSetup:
- 'spec/services/suspend_account_service_spec.rb' - 'spec/services/suspend_account_service_spec.rb'
- 'spec/services/unallow_domain_service_spec.rb' - 'spec/services/unallow_domain_service_spec.rb'
- 'spec/services/unsuspend_account_service_spec.rb' - 'spec/services/unsuspend_account_service_spec.rb'
- 'spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb'
- 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb'
RSpec/MessageChain: RSpec/MessageChain:
@ -394,7 +351,7 @@ RSpec/MessageSpies:
- 'spec/validators/status_length_validator_spec.rb' - 'spec/validators/status_length_validator_spec.rb'
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 19 Max: 8
# Configuration parameters: AllowSubject. # Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
@ -408,85 +365,6 @@ RSpec/PendingWithoutReason:
Exclude: Exclude:
- 'spec/models/account_spec.rb' - '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/controllers/api/v1/admin/domain_blocks_controller_spec.rb'
- 'spec/models/account_migration_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/relationship_filter_spec.rb'
- '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'
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
RSpec/VerifiedDoubles:
Exclude:
- 'spec/controllers/admin/change_emails_controller_spec.rb'
- 'spec/controllers/admin/confirmations_controller_spec.rb'
- 'spec/controllers/admin/disputes/appeals_controller_spec.rb'
- 'spec/controllers/admin/domain_allows_controller_spec.rb'
- 'spec/controllers/admin/domain_blocks_controller_spec.rb'
- 'spec/controllers/api/v1/reports_controller_spec.rb'
- 'spec/controllers/api/web/embeds_controller_spec.rb'
- 'spec/controllers/auth/sessions_controller_spec.rb'
- 'spec/controllers/disputes/appeals_controller_spec.rb'
- 'spec/helpers/statuses_helper_spec.rb'
- 'spec/lib/suspicious_sign_in_detector_spec.rb'
- 'spec/models/account/field_spec.rb'
- 'spec/models/session_activation_spec.rb'
- 'spec/models/setting_spec.rb'
- 'spec/services/account_search_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
- 'spec/services/search_service_spec.rb'
- 'spec/validators/blacklisted_email_validator_spec.rb'
- 'spec/validators/disallowed_hashtags_validator_spec.rb'
- 'spec/validators/email_mx_validator_spec.rb'
- 'spec/validators/follow_limit_validator_spec.rb'
- 'spec/validators/note_length_validator_spec.rb'
- 'spec/validators/poll_validator_spec.rb'
- 'spec/validators/status_length_validator_spec.rb'
- 'spec/validators/status_pin_validator_spec.rb'
- 'spec/validators/unique_username_validator_spec.rb'
- 'spec/validators/unreserved_username_validator_spec.rb'
- 'spec/validators/url_validator_spec.rb'
- 'spec/views/statuses/show.html.haml_spec.rb'
- 'spec/workers/activitypub/processing_worker_spec.rb'
- 'spec/workers/admin/domain_purge_worker_spec.rb'
- 'spec/workers/domain_block_worker_spec.rb'
- 'spec/workers/domain_clear_media_worker_spec.rb'
- 'spec/workers/feed_insert_worker_spec.rb'
- 'spec/workers/regeneration_worker_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
@ -597,7 +475,6 @@ Rails/NegateInclude:
- 'app/models/concerns/attachmentable.rb' - 'app/models/concerns/attachmentable.rb'
- 'app/models/concerns/remotable.rb' - 'app/models/concerns/remotable.rb'
- 'app/models/custom_filter.rb' - 'app/models/custom_filter.rb'
- 'app/models/webhook.rb'
- 'app/services/activitypub/process_status_update_service.rb' - 'app/services/activitypub/process_status_update_service.rb'
- 'app/services/fetch_link_card_service.rb' - 'app/services/fetch_link_card_service.rb'
- 'app/services/search_service.rb' - 'app/services/search_service.rb'
@ -773,11 +650,8 @@ Rails/WhereExists:
- 'app/workers/move_worker.rb' - 'app/workers/move_worker.rb'
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
- 'lib/tasks/tests.rake' - 'lib/tasks/tests.rake'
- 'spec/controllers/api/v1/accounts/notes_controller_spec.rb'
- 'spec/controllers/api/v1/tags_controller_spec.rb'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
- 'spec/services/activitypub/process_collection_service_spec.rb' - 'spec/services/activitypub/process_collection_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
- 'spec/services/purge_domain_service_spec.rb' - 'spec/services/purge_domain_service_spec.rb'
- 'spec/services/unallow_domain_service_spec.rb' - 'spec/services/unallow_domain_service_spec.rb'
@ -799,6 +673,7 @@ Style/ClassVars:
Exclude: Exclude:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/CombinableLoops: Style/CombinableLoops:
Exclude: Exclude:
- 'app/models/form/custom_emoji_batch.rb' - 'app/models/form/custom_emoji_batch.rb'
@ -835,406 +710,6 @@ Style/FormatStringToken:
- 'config/initializers/devise.rb' - 'config/initializers/devise.rb'
- 'lib/paperclip/color_extractor.rb' - 'lib/paperclip/color_extractor.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# 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/makara.rb'
- 'config/initializers/mime_types.rb'
- 'config/initializers/oj.rb'
- 'config/initializers/omniauth.rb'
- 'config/initializers/open_uri_redirection.rb'
- 'config/initializers/permissions_policy.rb'
- 'config/initializers/pghero.rb'
- 'config/initializers/preload_link_headers.rb'
- 'config/initializers/premailer_rails.rb'
- 'config/initializers/rack_attack_logging.rb'
- 'config/initializers/redis.rb'
- 'config/initializers/session_store.rb'
- 'config/initializers/simple_form.rb'
- 'config/initializers/stoplight.rb'
- 'config/initializers/trusted_proxies.rb'
- 'config/initializers/twitter_regex.rb'
- 'config/initializers/webauthn.rb'
- 'config/initializers/wrap_parameters.rb'
- 'config/locales/sr-Latn.rb'
- 'config/locales/sr.rb'
- 'config/puma.rb'
- 'db/migrate/20160220174730_create_accounts.rb'
- 'db/migrate/20160220211917_create_statuses.rb'
- 'db/migrate/20160221003140_create_users.rb'
- 'db/migrate/20160221003621_create_follows.rb'
- 'db/migrate/20160222122600_create_stream_entries.rb'
- 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb'
- 'db/migrate/20160223162837_add_metadata_to_statuses.rb'
- 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb'
- 'db/migrate/20160223165723_add_url_to_statuses.rb'
- 'db/migrate/20160223165855_add_url_to_accounts.rb'
- 'db/migrate/20160223171800_create_favourites.rb'
- 'db/migrate/20160224223247_create_mentions.rb'
- 'db/migrate/20160227230233_add_attachment_avatar_to_accounts.rb'
- 'db/migrate/20160305115639_add_devise_to_users.rb'
- 'db/migrate/20160306172223_create_doorkeeper_tables.rb'
- 'db/migrate/20160312193225_add_attachment_header_to_accounts.rb'
- 'db/migrate/20160314164231_add_owner_to_application.rb'
- 'db/migrate/20160316103650_add_missing_indices.rb'
- 'db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb'
- 'db/migrate/20160325130944_add_admin_to_users.rb'
- 'db/migrate/20160826155805_add_superapp_to_oauth_applications.rb'
- 'db/migrate/20160905150353_create_media_attachments.rb'
- 'db/migrate/20160919221059_add_subscription_expires_at_to_accounts.rb'
- 'db/migrate/20160920003904_remove_verify_token_from_accounts.rb'
- 'db/migrate/20160926213048_remove_owner_from_application.rb'
- 'db/migrate/20161003142332_add_confirmable_to_users.rb'
- 'db/migrate/20161003145426_create_blocks.rb'
- 'db/migrate/20161006213403_rails_settings_migration.rb'
- 'db/migrate/20161009120834_create_domain_blocks.rb'
- 'db/migrate/20161027172456_add_silenced_to_accounts.rb'
- 'db/migrate/20161104173623_create_tags.rb'
- 'db/migrate/20161105130633_create_statuses_tags_join_table.rb'
- 'db/migrate/20161116162355_add_locale_to_users.rb'
- 'db/migrate/20161119211120_create_notifications.rb'
- 'db/migrate/20161122163057_remove_unneeded_indexes.rb'
- 'db/migrate/20161123093447_add_sensitive_to_statuses.rb'
- 'db/migrate/20161128103007_create_subscriptions.rb'
- 'db/migrate/20161130142058_add_last_successful_delivery_at_to_subscriptions.rb'
- 'db/migrate/20161130185319_add_visibility_to_statuses.rb'
- 'db/migrate/20161202132159_add_in_reply_to_account_id_to_statuses.rb'
- 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb'
- 'db/migrate/20161205214545_add_suspended_to_accounts.rb'
- 'db/migrate/20161221152630_add_hidden_to_stream_entries.rb'
- 'db/migrate/20161222201034_add_locked_to_accounts.rb'
- 'db/migrate/20161222204147_create_follow_requests.rb'
- 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb'
- 'db/migrate/20170109120109_create_web_settings.rb'
- 'db/migrate/20170112154826_migrate_settings.rb'
- 'db/migrate/20170114194937_add_application_to_statuses.rb'
- 'db/migrate/20170114203041_add_website_to_oauth_application.rb'
- 'db/migrate/20170119214911_create_preview_cards.rb'
- 'db/migrate/20170123162658_add_severity_to_domain_blocks.rb'
- 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb'
- 'db/migrate/20170125145934_add_spoiler_text_to_statuses.rb'
- 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb'
- 'db/migrate/20170205175257_remove_devices.rb'
- 'db/migrate/20170209184350_add_reply_to_statuses.rb'
- 'db/migrate/20170214110202_create_reports.rb'
- 'db/migrate/20170217012631_add_reblog_of_id_foreign_key_to_statuses.rb'
- 'db/migrate/20170301222600_create_mutes.rb'
- 'db/migrate/20170303212857_add_last_emailed_at_to_users.rb'
- 'db/migrate/20170304202101_add_type_to_media_attachments.rb'
- 'db/migrate/20170317193015_add_search_index_to_accounts.rb'
- 'db/migrate/20170318214217_add_header_remote_url_to_accounts.rb'
- 'db/migrate/20170322021028_add_lowercase_index_to_accounts.rb'
- 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb'
- 'db/migrate/20170322162804_add_search_index_to_tags.rb'
- 'db/migrate/20170330021336_add_counter_caches.rb'
- 'db/migrate/20170330163835_create_imports.rb'
- 'db/migrate/20170330164118_add_attachment_data_to_imports.rb'
- 'db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb'
- 'db/migrate/20170405112956_add_index_on_mentions_status_id.rb'
- 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb'
- 'db/migrate/20170409170753_add_last_webfingered_at_to_accounts.rb'
- 'db/migrate/20170414080609_add_devise_two_factor_backupable_to_users.rb'
- 'db/migrate/20170414132105_add_language_to_statuses.rb'
- 'db/migrate/20170418160728_add_indexes_to_reports_for_accounts.rb'
- 'db/migrate/20170423005413_add_allowed_languages_to_user.rb'
- 'db/migrate/20170424003227_create_account_domain_blocks.rb'
- 'db/migrate/20170424112722_add_status_id_index_to_statuses_tags.rb'
- 'db/migrate/20170425131920_add_media_attachment_meta.rb'
- 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb'
- 'db/migrate/20170427011934_re_add_owner_to_application.rb'
- 'db/migrate/20170506235850_create_conversations.rb'
- 'db/migrate/20170507000211_add_conversation_id_to_statuses.rb'
- 'db/migrate/20170507141759_optimize_index_subscriptions.rb'
- 'db/migrate/20170508230434_create_conversation_mutes.rb'
- 'db/migrate/20170516072309_add_index_accounts_on_uri.rb'
- 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb'
- 'db/migrate/20170601210557_add_index_on_media_attachments_account_id.rb'
- 'db/migrate/20170604144747_add_foreign_keys_for_accounts.rb'
- 'db/migrate/20170606113804_change_tag_search_index_to_btree.rb'
- 'db/migrate/20170609145826_remove_default_language_from_statuses.rb'
- 'db/migrate/20170610000000_add_statuses_index_on_account_id_id.rb'
- 'db/migrate/20170623152212_create_session_activations.rb'
- 'db/migrate/20170624134742_add_description_to_session_activations.rb'
- 'db/migrate/20170625140443_add_access_token_id_to_session_activations.rb'
- 'db/migrate/20170711225116_fix_null_booleans.rb'
- 'db/migrate/20170713112503_make_tag_search_case_insensitive.rb'
- 'db/migrate/20170713175513_create_web_push_subscriptions.rb'
- 'db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb'
- 'db/migrate/20170714184731_add_domain_to_subscriptions.rb'
- 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb'
- 'db/migrate/20170718211102_add_activitypub_to_accounts.rb'
- 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb'
- 'db/migrate/20170823162448_create_status_pins.rb'
- 'db/migrate/20170824103029_add_timestamps_to_status_pins.rb'
- 'db/migrate/20170829215220_remove_status_pins_account_index.rb'
- 'db/migrate/20170901141119_truncate_preview_cards.rb'
- 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb'
- 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb'
- 'db/migrate/20170905165803_add_local_to_statuses.rb'
- 'db/migrate/20170913000752_create_site_uploads.rb'
- 'db/migrate/20170917153509_create_custom_emojis.rb'
- 'db/migrate/20170918125918_ids_to_bigints.rb'
- 'db/migrate/20170920024819_status_ids_to_timestamp_ids.rb'
- 'db/migrate/20170920032311_fix_reblogs_in_feeds.rb'
- 'db/migrate/20170924022025_ids_to_bigints2.rb'
- 'db/migrate/20170927215609_add_description_to_media_attachments.rb'
- 'db/migrate/20170928082043_create_email_domain_blocks.rb'
- 'db/migrate/20171005102658_create_account_moderation_notes.rb'
- 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb'
- 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb'
- 'db/migrate/20171010023049_add_foreign_key_to_account_moderation_notes.rb'
- 'db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb'
- 'db/migrate/20171020084748_add_visible_in_picker_to_custom_emoji.rb'
- 'db/migrate/20171028221157_add_reblogs_to_follows.rb'
- 'db/migrate/20171107143332_add_memorial_to_accounts.rb'
- 'db/migrate/20171107143624_add_disabled_to_users.rb'
- 'db/migrate/20171109012327_add_moderator_to_accounts.rb'
- 'db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb'
- 'db/migrate/20171114231651_create_lists.rb'
- 'db/migrate/20171116161857_create_list_accounts.rb'
- 'db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb'
- 'db/migrate/20171119172437_create_admin_action_logs.rb'
- 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb'
- 'db/migrate/20171125024930_create_invites.rb'
- 'db/migrate/20171125031751_add_invite_id_to_users.rb'
- 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb'
- 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb'
- 'db/migrate/20171129172043_add_index_on_stream_entries.rb'
- 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb'
- 'db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb'
- 'db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb'
- 'db/migrate/20171226094803_more_faster_index_on_notifications.rb'
- 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
- 'db/migrate/20180109143959_add_remember_token_to_users.rb'
- 'db/migrate/20180204034416_create_identities.rb'
- 'db/migrate/20180206000000_change_user_id_nonnullable.rb'
- 'db/migrate/20180211015820_create_backups.rb'
- 'db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb'
- 'db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb'
- 'db/migrate/20180402031200_add_assigned_account_id_to_reports.rb'
- 'db/migrate/20180402040909_create_report_notes.rb'
- 'db/migrate/20180410204633_add_fields_to_accounts.rb'
- 'db/migrate/20180416210259_add_uri_to_relationships.rb'
- 'db/migrate/20180506221944_add_actor_type_to_accounts.rb'
- 'db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb'
- 'db/migrate/20180510230049_migrate_web_push_subscriptions.rb'
- 'db/migrate/20180528141303_fix_accounts_unique_index.rb'
- 'db/migrate/20180608213548_reject_following_blocked_users.rb'
- 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb'
- 'db/migrate/20180615122121_add_autofollow_to_invites.rb'
- 'db/migrate/20180616192031_add_chosen_languages_to_users.rb'
- 'db/migrate/20180617162849_remove_unused_indexes.rb'
- 'db/migrate/20180628181026_create_custom_filters.rb'
- 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb'
- 'db/migrate/20180711152640_create_relays.rb'
- 'db/migrate/20180808175627_create_account_pins.rb'
- 'db/migrate/20180812123222_change_relays_enabled.rb'
- 'db/migrate/20180812162710_create_status_stats.rb'
- 'db/migrate/20180812173710_copy_status_stats.rb'
- 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb'
- 'db/migrate/20180831171112_create_bookmarks.rb'
- 'db/migrate/20180929222014_create_account_conversations.rb'
- 'db/migrate/20181007025445_create_pghero_space_stats.rb'
- 'db/migrate/20181010141500_add_silent_to_mentions.rb'
- 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb'
- 'db/migrate/20181018205649_add_unread_to_account_conversations.rb'
- 'db/migrate/20181024224956_migrate_account_conversations.rb'
- 'db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb'
- 'db/migrate/20181116165755_create_account_stats.rb'
- 'db/migrate/20181116173541_copy_account_stats.rb'
- 'db/migrate/20181127130500_identity_id_to_bigint.rb'
- 'db/migrate/20181127165847_add_show_replies_to_lists.rb'
- 'db/migrate/20181203003808_create_accounts_tags_join_table.rb'
- 'db/migrate/20181203021853_add_discoverable_to_accounts.rb'
- 'db/migrate/20181204193439_add_last_status_at_to_account_stats.rb'
- 'db/migrate/20181204215309_create_account_tag_stats.rb'
- 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb'
- 'db/migrate/20181213184704_create_account_warnings.rb'
- 'db/migrate/20181213185533_create_account_warning_presets.rb'
- 'db/migrate/20181219235220_add_created_by_application_id_to_users.rb'
- 'db/migrate/20181226021420_add_also_known_as_to_accounts.rb'
- 'db/migrate/20190103124649_create_scheduled_statuses.rb'
- 'db/migrate/20190103124754_add_scheduled_status_id_to_media_attachments.rb'
- 'db/migrate/20190117114553_create_tombstones.rb'
- 'db/migrate/20190201012802_add_overwrite_to_imports.rb'
- 'db/migrate/20190203180359_create_featured_tags.rb'
- 'db/migrate/20190225031541_create_polls.rb'
- 'db/migrate/20190225031625_create_poll_votes.rb'
- 'db/migrate/20190226003449_add_poll_id_to_statuses.rb'
- 'db/migrate/20190304152020_add_uri_to_poll_votes.rb'
- 'db/migrate/20190306145741_add_lock_version_to_polls.rb'
- 'db/migrate/20190307234537_add_approved_to_users.rb'
- 'db/migrate/20190314181829_migrate_open_registrations_setting.rb'
- 'db/migrate/20190316190352_create_account_identity_proofs.rb'
- 'db/migrate/20190317135723_add_uri_to_reports.rb'
- 'db/migrate/20190403141604_add_comment_to_invites.rb'
- 'db/migrate/20190409054914_create_user_invite_requests.rb'
- 'db/migrate/20190420025523_add_blurhash_to_media_attachments.rb'
- 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb'
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
- 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
- 'db/migrate/20190627222225_create_custom_emoji_categories.rb'
- 'db/migrate/20190627222826_add_category_id_to_custom_emojis.rb'
- 'db/migrate/20190701022101_add_trust_level_to_accounts.rb'
- 'db/migrate/20190705002136_create_domain_allows.rb'
- 'db/migrate/20190715164535_add_instance_actor.rb'
- 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb'
- 'db/migrate/20190729185330_add_score_to_tags.rb'
- 'db/migrate/20190805123746_add_capabilities_to_tags.rb'
- 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb'
- 'db/migrate/20190815225426_add_last_status_at_to_tags.rb'
- 'db/migrate/20190819134503_add_deleted_at_to_statuses.rb'
- 'db/migrate/20190820003045_update_statuses_index.rb'
- 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
- 'db/migrate/20190901035623_add_max_score_to_tags.rb'
- 'db/migrate/20190904222339_create_markers.rb'
- 'db/migrate/20190914202517_create_account_migrations.rb'
- 'db/migrate/20190915194355_create_account_aliases.rb'
- 'db/migrate/20190927232842_add_voters_count_to_polls.rb'
- 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb'
- 'db/migrate/20191007013357_update_pt_locales.rb'
- 'db/migrate/20191031163205_change_list_account_follow_nullable.rb'
- 'db/migrate/20191212003415_increase_backup_size.rb'
- 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb'
- 'db/migrate/20191218153258_create_announcements.rb'
- 'db/migrate/20200113125135_create_announcement_mutes.rb'
- 'db/migrate/20200114113335_create_announcement_reactions.rb'
- 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
- 'db/migrate/20200126203551_add_published_at_to_announcements.rb'
- 'db/migrate/20200306035625_add_processing_to_media_attachments.rb'
- 'db/migrate/20200309150742_add_forwarded_to_reports.rb'
- 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb'
- 'db/migrate/20200312162302_add_status_ids_to_announcements.rb'
- 'db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb'
- 'db/migrate/20200317021758_add_expires_at_to_mutes.rb'
- 'db/migrate/20200407201300_create_unavailable_domains.rb'
- 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
- 'db/migrate/20200417125749_add_storage_schema_version.rb'
- 'db/migrate/20200508212852_reset_unique_jobs_locks.rb'
- 'db/migrate/20200510110808_reset_web_app_secret.rb'
- 'db/migrate/20200510181721_remove_duplicated_indexes_pghero.rb'
- 'db/migrate/20200516180352_create_devices.rb'
- 'db/migrate/20200516183822_create_one_time_keys.rb'
- 'db/migrate/20200518083523_create_encrypted_messages.rb'
- 'db/migrate/20200521180606_encrypted_message_ids_to_timestamp_ids.rb'
- 'db/migrate/20200529214050_add_devices_url_to_accounts.rb'
- 'db/migrate/20200601222558_create_system_keys.rb'
- 'db/migrate/20200605155027_add_blurhash_to_preview_cards.rb'
- 'db/migrate/20200608113046_add_sign_in_token_to_users.rb'
- 'db/migrate/20200614002136_add_sensitized_to_accounts.rb'
- 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb'
- 'db/migrate/20200622213645_media_attachment_ids_to_timestamp_ids.rb'
- 'db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb'
- 'db/migrate/20200628133322_create_account_notes.rb'
- 'db/migrate/20200630190240_create_webauthn_credentials.rb'
- 'db/migrate/20200630190544_add_webauthn_id_to_users.rb'
- 'db/migrate/20200908193330_create_account_deletion_requests.rb'
- 'db/migrate/20200917192924_add_notify_to_follows.rb'
- 'db/migrate/20200917193034_add_type_to_notifications.rb'
- 'db/migrate/20200917222316_add_index_notifications_on_type.rb'
- 'db/migrate/20201008202037_create_ip_blocks.rb'
- 'db/migrate/20201008220312_add_sign_up_ip_to_users.rb'
- 'db/migrate/20201017233919_add_suspension_origin_to_accounts.rb'
- 'db/migrate/20201206004238_create_instances.rb'
- 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb'
- 'db/migrate/20210221045109_create_rules.rb'
- 'db/migrate/20210306164523_account_ids_to_timestamp_ids.rb'
- 'db/migrate/20210322164601_create_account_summaries.rb'
- 'db/migrate/20210323114347_create_follow_recommendations.rb'
- 'db/migrate/20210324171613_create_follow_recommendation_suppressions.rb'
- 'db/migrate/20210416200740_create_canonical_email_blocks.rb'
- 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb'
- 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb'
- 'db/migrate/20210505174616_update_follow_recommendations_to_version_2.rb'
- 'db/migrate/20210609202149_create_login_activities.rb'
- 'db/migrate/20210616214526_create_user_ips.rb'
- 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb'
- 'db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb'
- 'db/migrate/20210722120340_create_account_statuses_cleanup_policies.rb'
- 'db/migrate/20210904215403_add_edited_at_to_statuses.rb'
- 'db/migrate/20210908220918_create_status_edits.rb'
- 'db/migrate/20211031031021_create_preview_card_providers.rb'
- 'db/migrate/20211112011713_add_language_to_preview_cards.rb'
- 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb'
- 'db/migrate/20211123212714_add_link_type_to_preview_cards.rb'
- 'db/migrate/20211213040746_update_account_summaries_to_version_2.rb'
- 'db/migrate/20211231080958_add_category_to_reports.rb'
- 'db/migrate/20220105163928_remove_mentions_status_id_index.rb'
- 'db/migrate/20220115125126_add_report_id_to_account_warnings.rb'
- 'db/migrate/20220115125341_fix_account_warning_actions.rb'
- 'db/migrate/20220116202951_add_deleted_at_index_on_statuses.rb'
- 'db/migrate/20220124141035_create_appeals.rb'
- 'db/migrate/20220202200743_add_trendable_to_accounts.rb'
- 'db/migrate/20220202200926_add_trendable_to_statuses.rb'
- 'db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb'
- 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb'
- 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb'
- 'db/migrate/20220302232632_add_ordered_media_attachment_ids_to_statuses.rb'
- 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb'
- 'db/migrate/20220304195405_migrate_hide_network_preference.rb'
- 'db/migrate/20220307094650_fix_featured_tags_constraints.rb'
- 'db/migrate/20220309213005_fix_reblog_deleted_at.rb'
- 'db/migrate/20220316233212_update_kurdish_locales.rb'
- 'db/migrate/20220428112511_add_index_statuses_on_account_id.rb'
- 'db/migrate/20220428112727_add_index_statuses_pins_on_status_id.rb'
- 'db/migrate/20220428114454_add_index_reports_on_assigned_account_id.rb'
- 'db/migrate/20220428114902_add_index_reports_on_action_taken_by_account_id.rb'
- 'db/migrate/20220606044941_create_webhooks.rb'
- 'db/migrate/20220611210335_create_user_roles.rb'
- 'db/migrate/20220611212541_add_role_id_to_users.rb'
- 'db/migrate/20220710102457_add_display_name_to_tags.rb'
- 'db/migrate/20220714171049_create_tag_follows.rb'
- 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb'
- 'db/migrate/20220824233535_create_status_trends.rb'
- 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb'
- 'db/migrate/20220829192633_add_languages_to_follows.rb'
- 'db/migrate/20220829192658_add_languages_to_follow_requests.rb'
- 'db/migrate/20221006061337_create_preview_card_trends.rb'
- 'db/migrate/20221012181003_add_blurhash_to_site_uploads.rb'
- 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb'
- 'db/migrate/20221025171544_add_index_ip_blocks_on_ip.rb'
- 'db/migrate/20221104133904_add_name_to_featured_tags.rb'
- 'db/post_migrate/20190519130537_remove_boosts_widening_audience.rb'
- 'db/post_migrate/20210308133107_remove_subscription_expires_at_from_accounts.rb'
- 'db/post_migrate/20220118183123_remove_rememberable_from_users.rb'
- 'db/seeds/01_web_app.rb'
- 'db/seeds/02_instance_actor.rb'
- 'db/seeds/03_roles.rb'
- 'db/seeds/04_admin.rb'
- 'lib/rails/engine_extensions.rb'
- 'lib/tasks/branding.rake'
- 'spec/fabricators_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream: Style/GlobalStdStream:
Exclude: Exclude:
@ -1242,11 +717,6 @@ Style/GlobalStdStream:
- 'config/environments/development.rb' - 'config/environments/development.rb'
- 'config/environments/production.rb' - 'config/environments/production.rb'
# Configuration parameters: AllowedVariables.
Style/GlobalVars:
Exclude:
- 'config/initializers/statsd.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
Style/GuardClause: Style/GuardClause:
@ -1380,7 +850,6 @@ Style/RedundantConstantBase:
Exclude: Exclude:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/sidekiq.rb' - 'config/initializers/sidekiq.rb'
- 'config/initializers/statsd.rb'
- 'config/locales/sr-Latn.rb' - 'config/locales/sr-Latn.rb'
- 'config/locales/sr.rb' - 'config/locales/sr.rb'
@ -1402,13 +871,6 @@ Style/SafeNavigation:
- 'app/models/concerns/account_finder_concern.rb' - 'app/models/concerns/account_finder_concern.rb'
- 'app/models/status.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). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: only_raise, only_fail, semantic # SupportedStyles: only_raise, only_fail, semantic
@ -1422,21 +884,6 @@ Style/SingleArgumentDig:
Exclude: Exclude:
- 'lib/webpacker/manifest_extensions.rb' - '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). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: require_parentheses, require_no_parentheses # 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 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)) - 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: - 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 `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 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)) - Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895))

View file

@ -3,17 +3,14 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 3.0.0' ruby '>= 3.0.0'
gem 'pkg-config', '~> 1.5'
gem 'puma', '~> 6.3' gem 'puma', '~> 6.3'
gem 'rails', '~> 6.1.7' gem 'rails', '~> 7.0'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2' gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.7' gem 'rack', '~> 2.2.7'
gem 'haml-rails', '~>2.0' gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.5' gem 'pg', '~> 1.5'
gem 'makara', '~> 0.5'
gem 'pghero' gem 'pghero'
gem 'dotenv-rails', '~> 2.8' gem 'dotenv-rails', '~> 2.8'
@ -69,7 +66,7 @@ gem 'pundit', '~> 2.3'
gem 'premailer-rails' gem 'premailer-rails'
gem 'rack-attack', '~> 6.6' gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rack-cors', '~> 2.0', require: 'rack/cors'
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 '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 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
@ -161,7 +158,7 @@ group :development do
gem 'letter_opener_web', '~> 2.0' gem 'letter_opener_web', '~> 2.0'
# Security analysis CLI tools # Security analysis CLI tools
gem 'brakeman', '~> 5.4', require: false gem 'brakeman', '~> 6.0', require: false
gem 'bundler-audit', '~> 0.9', require: false gem 'bundler-audit', '~> 0.9', require: false
# Linter CLI for HAML files # Linter CLI for HAML files

View file

@ -18,40 +18,47 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.7.3) actioncable (7.0.6)
actionpack (= 6.1.7.3) actionpack (= 7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.3) actionmailbox (7.0.6)
actionpack (= 6.1.7.3) actionpack (= 7.0.6)
activejob (= 6.1.7.3) activejob (= 7.0.6)
activerecord (= 6.1.7.3) activerecord (= 7.0.6)
activestorage (= 6.1.7.3) activestorage (= 7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.7.3) net-imap
actionpack (= 6.1.7.3) net-pop
actionview (= 6.1.7.3) net-smtp
activejob (= 6.1.7.3) actionmailer (7.0.6)
activesupport (= 6.1.7.3) actionpack (= 7.0.6)
actionview (= 7.0.6)
activejob (= 7.0.6)
activesupport (= 7.0.6)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.7.3) actionpack (7.0.6)
actionview (= 6.1.7.3) actionview (= 7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (6.1.7.3) actiontext (7.0.6)
actionpack (= 6.1.7.3) actionpack (= 7.0.6)
activerecord (= 6.1.7.3) activerecord (= 7.0.6)
activestorage (= 6.1.7.3) activestorage (= 7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.7.3) actionview (7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -61,27 +68,26 @@ GEM
activemodel (>= 4.1, < 7.1) activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (6.1.7.3) activejob (7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.7.3) activemodel (7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
activerecord (6.1.7.3) activerecord (7.0.6)
activemodel (= 6.1.7.3) activemodel (= 7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
activestorage (6.1.7.3) activestorage (7.0.6)
actionpack (= 6.1.7.3) actionpack (= 7.0.6)
activejob (= 6.1.7.3) activejob (= 7.0.6)
activerecord (= 6.1.7.3) activerecord (= 7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.7.3) activesupport (7.0.6)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.4) addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0) aes_key_wrap (1.1.0)
@ -97,26 +103,26 @@ GEM
attr_required (1.0.1) attr_required (1.0.1)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.772.0) aws-partitions (1.780.0)
aws-sdk-core (3.174.0) aws-sdk-core (3.175.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.65.0) aws-sdk-kms (1.67.0)
aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-core (~> 3, >= 3.174.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.123.0) aws-sdk-s3 (1.126.0)
aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-core (~> 3, >= 3.174.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2) aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.18) bcrypt (3.1.18)
better_errors (2.9.1) better_errors (2.10.1)
coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
rouge (>= 1.0.0)
better_html (2.0.1) better_html (2.0.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
@ -130,7 +136,7 @@ GEM
blurhash (0.1.7) blurhash (0.1.7)
bootsnap (1.16.0) bootsnap (1.16.0)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (5.4.1) brakeman (6.0.0)
browser (5.3.1) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
@ -146,7 +152,7 @@ GEM
sshkit (>= 1.9.0) sshkit (>= 1.9.0)
capistrano-bundler (2.1.0) capistrano-bundler (2.1.0)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-rails (1.6.2) capistrano-rails (1.6.3)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-bundler (>= 1.1, < 3) capistrano-bundler (>= 1.1, < 3)
capistrano-rbenv (2.2.0) capistrano-rbenv (2.2.0)
@ -154,7 +160,7 @@ GEM
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (3.39.1) capybara (3.39.2)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
@ -167,14 +173,13 @@ GEM
activesupport activesupport
cbor (0.5.9.6) cbor (0.5.9.6)
charlock_holmes (0.7.7) charlock_holmes (0.7.7)
chewy (7.3.2) chewy (7.3.3)
activesupport (>= 5.2) activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl elasticsearch-dsl
chunky_png (1.4.0) chunky_png (1.4.0)
climate_control (0.2.0) climate_control (0.2.0)
cocoon (1.2.15) cocoon (1.2.15)
coderay (1.1.3)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.2.2) concurrent-ruby (1.2.2)
connection_pool (2.4.1) connection_pool (2.4.1)
@ -229,7 +234,7 @@ GEM
erubi (1.12.0) erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
excon (0.99.0) excon (0.100.0)
fabrication (2.30.0) fabrication (2.30.0)
faker (3.2.0) faker (3.2.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
@ -292,11 +297,11 @@ GEM
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.45.0) haml_lint (0.48.0)
haml (>= 4.0, < 6.2) haml (>= 4.0, < 6.2)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
rubocop (>= 0.50.0) rubocop (>= 1.0)
sysexits (~> 1.1) sysexits (~> 1.1)
hashdiff (1.0.1) hashdiff (1.0.1)
hashie (5.0.0) hashie (5.0.0)
@ -319,7 +324,7 @@ GEM
httplog (1.6.2) httplog (1.6.2)
rack (>= 2.0) rack (>= 2.0)
rainbow (>= 2.0.0) rainbow (>= 2.0.0)
i18n (1.13.0) i18n (1.14.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-tasks (1.0.12) i18n-tasks (1.0.12)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
@ -355,7 +360,7 @@ GEM
json-schema (4.0.0) json-schema (4.0.0)
addressable (>= 2.8) addressable (>= 2.8)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.7.0) jwt (2.7.1)
kaminari (1.2.2) kaminari (1.2.2)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2) kaminari-actionview (= 1.2.2)
@ -374,6 +379,7 @@ GEM
marcel (~> 1.0.1) marcel (~> 1.0.1)
mime-types mime-types
terrapin (~> 0.6.0) terrapin (~> 0.6.0)
language_server-protocol (3.17.0.3)
launchy (2.5.2) launchy (2.5.2)
addressable (~> 2.8) addressable (~> 2.8)
letter_opener (1.8.1) letter_opener (1.8.1)
@ -400,8 +406,6 @@ GEM
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
makara (0.5.1)
activerecord (>= 5.2.0)
marcel (1.0.2) marcel (1.0.2)
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
@ -413,13 +417,13 @@ GEM
mime-types-data (3.2023.0218.1) mime-types-data (3.2023.0218.1)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.2) mini_portile2 (2.8.2)
minitest (5.18.0) minitest (5.18.1)
msgpack (1.7.0) msgpack (1.7.1)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
net-http (0.3.2) net-http (0.3.2)
uri uri
net-imap (0.3.4) net-imap (0.3.6)
date date
net-protocol net-protocol
net-ldap (0.18.0) net-ldap (0.18.0)
@ -433,10 +437,10 @@ GEM
net-protocol net-protocol
net-ssh (7.1.0) net-ssh (7.1.0)
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.15.2) nokogiri (1.15.3)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oj (3.14.3) oj (3.15.0)
omniauth (1.9.2) omniauth (1.9.2)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
@ -470,15 +474,15 @@ GEM
orm_adapter (0.5.0) orm_adapter (0.5.0)
ox (2.14.16) ox (2.14.16)
parallel (1.23.0) parallel (1.23.0)
parser (3.2.2.1) parser (3.2.2.3)
ast (~> 2.4.1) ast (~> 2.4.1)
racc
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.3) pg (1.5.3)
pghero (3.3.3) pghero (3.3.3)
activerecord (>= 6) activerecord (>= 6)
pkg-config (1.5.1)
posix-spawn (0.3.15) posix-spawn (0.3.15)
premailer (1.21.0) premailer (1.21.0)
addressable addressable
@ -495,7 +499,7 @@ GEM
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.2) racc (1.7.1)
rack (2.2.7) rack (2.2.7)
rack-attack (6.6.1) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
@ -511,21 +515,20 @@ GEM
rack rack
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rails (6.1.7.3) rails (7.0.6)
actioncable (= 6.1.7.3) actioncable (= 7.0.6)
actionmailbox (= 6.1.7.3) actionmailbox (= 7.0.6)
actionmailer (= 6.1.7.3) actionmailer (= 7.0.6)
actionpack (= 6.1.7.3) actionpack (= 7.0.6)
actiontext (= 6.1.7.3) actiontext (= 7.0.6)
actionview (= 6.1.7.3) actionview (= 7.0.6)
activejob (= 6.1.7.3) activejob (= 7.0.6)
activemodel (= 6.1.7.3) activemodel (= 7.0.6)
activerecord (= 6.1.7.3) activerecord (= 7.0.6)
activestorage (= 6.1.7.3) activestorage (= 7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.7.3) railties (= 7.0.6)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
@ -536,28 +539,29 @@ GEM
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (~> 1.14)
rails-i18n (6.0.0) rails-i18n (7.0.7)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 8)
railties (6.1.7.3) railties (7.0.6)
actionpack (= 6.1.7.3) actionpack (= 7.0.6)
activesupport (= 6.1.7.3) activesupport (= 7.0.6)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.0.6)
rdf (3.2.10) rdf (3.2.11)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.5.1) rdf-normalize (0.6.0)
rdf (~> 3.2) rdf (~> 3.2)
redcarpet (3.6.0) redcarpet (3.6.0)
redis (4.8.1) redis (4.8.1)
redis-namespace (1.10.0) redis-namespace (1.11.0)
redis (>= 4) redis (>= 4)
redlock (1.3.2) redlock (1.3.2)
redis (>= 3.0.0, < 6.0) redis (>= 3.0.0, < 6.0)
regexp_parser (2.8.0) regexp_parser (2.8.1)
request_store (1.5.1) request_store (1.5.1)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.0) responders (3.1.0)
@ -565,6 +569,7 @@ GEM
railties (>= 5.2) railties (>= 5.2)
rexml (3.2.5) rexml (3.2.5)
rotp (6.2.2) rotp (6.2.2)
rouge (4.1.2)
rpam2 (4.0.2) rpam2 (4.0.2)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
@ -591,41 +596,45 @@ GEM
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.12.0) rspec-support (3.12.0)
rspec_chunked (0.6) rspec_chunked (0.6)
rubocop (1.51.0) rubocop (1.54.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.0.0) parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0) rubocop-ast (>= 1.28.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.28.1) rubocop-ast (1.29.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.18.0) rubocop-capybara (2.18.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-performance (1.18.0) rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.19.1) rubocop-rails (2.20.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.19.0) rubocop-rspec (2.22.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.13.0) ruby-saml (1.15.0)
nokogiri (>= 1.10.5) nokogiri (>= 1.13.10)
rexml rexml
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
rufus-scheduler (3.8.2) rufus-scheduler (3.9.1)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0) safety_net_attestation (0.4.0)
jwt (~> 2.0) jwt (~> 2.0)
sanitize (6.0.1) sanitize (6.0.2)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
scenic (1.7.0) scenic (1.7.0)
@ -667,7 +676,7 @@ GEM
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.21.4) sshkit (1.21.5)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.25) stackprof (0.2.25)
@ -680,14 +689,14 @@ GEM
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
httpclient (>= 2.4) httpclient (>= 2.4)
sysexits (1.2.0) sysexits (1.2.0)
temple (0.10.0) temple (0.10.2)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
thor (1.2.2) thor (1.2.2)
tilt (2.1.0) tilt (2.2.0)
timeout (0.3.2) timeout (0.4.0)
tpm-key_attestation (0.12.0) tpm-key_attestation (0.12.0)
bindata (~> 2.4) bindata (~> 2.4)
openssl (> 2.0) openssl (> 2.0)
@ -713,7 +722,7 @@ GEM
unf_ext unf_ext
unf_ext (0.0.8.2) unf_ext (0.0.8.2)
unicode-display_width (2.4.2) unicode-display_width (2.4.2)
uri (0.12.1) uri (0.12.2)
validate_email (0.1.6) validate_email (0.1.6)
activemodel (>= 3.0) activemodel (>= 3.0)
mail (>= 2.2.5) mail (>= 2.2.5)
@ -764,7 +773,7 @@ DEPENDENCIES
binding_of_caller (~> 1.0) binding_of_caller (~> 1.0)
blurhash (~> 0.1) blurhash (~> 0.1)
bootsnap (~> 1.16.0) bootsnap (~> 1.16.0)
brakeman (~> 5.4) brakeman (~> 6.0)
browser browser
bundler-audit (~> 0.9) bundler-audit (~> 0.9)
capistrano (~> 3.17) capistrano (~> 3.17)
@ -812,7 +821,6 @@ DEPENDENCIES
letter_opener_web (~> 2.0) letter_opener_web (~> 2.0)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.12) lograge (~> 0.12)
makara (~> 0.5)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
mime-types (~> 3.4.1) mime-types (~> 3.4.1)
@ -829,7 +837,6 @@ DEPENDENCIES
parslet parslet
pg (~> 1.5) pg (~> 1.5)
pghero pghero
pkg-config (~> 1.5)
posix-spawn posix-spawn
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
@ -840,9 +847,9 @@ DEPENDENCIES
rack-attack (~> 6.6) rack-attack (~> 6.6)
rack-cors (~> 2.0) rack-cors (~> 2.0)
rack-test (~> 2.1) rack-test (~> 2.1)
rails (~> 6.1.7) rails (~> 7.0)
rails-controller-testing (~> 1.0) rails-controller-testing (~> 1.0)
rails-i18n (~> 6.0) rails-i18n (~> 7.0)
rails-settings-cached (~> 0.6)! rails-settings-cached (~> 0.6)!
rdf-normalize (~> 0.5) rdf-normalize (~> 0.5)
redcarpet (~> 3.6) redcarpet (~> 3.6)
@ -887,4 +894,4 @@ RUBY VERSION
ruby 3.2.2p53 ruby 3.2.2p53
BUNDLED WITH BUNDLED WITH
2.4.10 2.4.13

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
# Add your own tasks in files placed in lib/tasks ending in .rake, # 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. # 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 Rails.application.load_tasks

View file

@ -2,8 +2,37 @@
class AccountsIndex < Chewy::Index class AccountsIndex < Chewy::Index
settings index: { refresh_interval: '30s' }, analysis: { settings index: { refresh_interval: '30s' }, analysis: {
filter: {
english_stop: {
type: 'stop',
stopwords: '_english_',
},
english_stemmer: {
type: 'stemmer',
language: 'english',
},
english_possessive_stemmer: {
type: 'stemmer',
language: 'possessive_english',
},
},
analyzer: { analyzer: {
content: { natural: {
tokenizer: 'uax_url_email',
filter: %w(
english_possessive_stemmer
lowercase
asciifolding
cjk_width
english_stop
english_stemmer
),
},
verbatim: {
tokenizer: 'whitespace', tokenizer: 'whitespace',
filter: %w(lowercase asciifolding cjk_width), filter: %w(lowercase asciifolding cjk_width),
}, },
@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index
index_scope ::Account.searchable.includes(:account_stat) index_scope ::Account.searchable.includes(:account_stat)
root date_detection: false do root date_detection: false do
field :id, type: 'long' field(:id, type: 'long')
field(:following_count, type: 'long', value: ->(account) { account.public_following_count })
field :display_name, type: 'text', analyzer: 'content' do field(:followers_count, type: 'long', value: ->(account) { account.public_followers_count })
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
end field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
end
field :following_count, type: 'long', value: ->(account) { account.public_following_count }
field :followers_count, type: 'long', value: ->(account) { account.public_followers_count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
end end
end end

View file

@ -14,15 +14,5 @@ module Admin
@pending_tags_count = Tag.pending_review.count @pending_tags_count = Tag.pending_review.count
@pending_appeals_count = Appeal.pending.count @pending_appeals_count = Appeal.pending.count
end end
private
def redis_info
@redis_info ||= if redis.is_a?(Redis::Namespace)
redis.redis.info
else
redis.info
end
end
end end
end end

View file

@ -28,6 +28,7 @@ module Admin
authorize :webhook, :create? authorize :webhook, :create?
@webhook = Webhook.new(resource_params) @webhook = Webhook.new(resource_params)
@webhook.current_account = current_account
if @webhook.save if @webhook.save
redirect_to admin_webhook_path(@webhook) redirect_to admin_webhook_path(@webhook)
@ -39,10 +40,12 @@ module Admin
def update def update
authorize @webhook, :update? authorize @webhook, :update?
@webhook.current_account = current_account
if @webhook.update(resource_params) if @webhook.update(resource_params)
redirect_to admin_webhook_path(@webhook) redirect_to admin_webhook_path(@webhook)
else else
render :show render :edit
end end
end end

View file

@ -90,7 +90,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def account_params def account_params
params.permit(:username, :email, :password, :agreement, :locale, :reason) params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone)
end end
def check_enabled_registrations def check_enabled_registrations

View file

@ -21,7 +21,7 @@ class Api::V1::BookmarksController < Api::BaseController
end end
def results 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), limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id) params_slice(:max_id, :since_id, :min_id)
) )

View file

@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController
render json: @conversation, serializer: REST::ConversationSerializer render json: @conversation, serializer: REST::ConversationSerializer
end end
def unread
@conversation.update!(unread: true)
render json: @conversation, serializer: REST::ConversationSerializer
end
def destroy def destroy
@conversation.destroy! @conversation.destroy!
render_empty render_empty
@ -45,7 +50,7 @@ class Api::V1::ConversationsController < Api::BaseController
}, },
] ]
) )
.to_a_paginated_by_id(limit_param(LIMIT), **params_slice(:max_id, :since_id, :min_id)) .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end end
def insert_pagination_headers def insert_pagination_headers

View file

@ -21,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController
def accounts_scope def accounts_scope
Account.discoverable.tap do |scope| Account.discoverable.tap do |scope|
scope.merge!(Account.local) if truthy_param?(:local) scope.merge!(account_order_scope)
scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active' scope.merge!(local_account_scope) if local_accounts?
scope.merge!(Account.order(id: :desc)) if params[:order] == 'new' scope.merge!(account_exclusion_scope) if current_account
scope.merge!(Account.not_excluded_by_account(current_account)) if current_account scope.merge!(account_domain_block_scope) if current_account && !local_accounts?
scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
end end
end end
def local_accounts?
truthy_param?(:local)
end
def account_order_scope
case params[:order]
when 'new'
Account.order(id: :desc)
when 'active', nil
Account.by_recent_status
end
end
def local_account_scope
Account.local
end
def account_exclusion_scope
Account.not_excluded_by_account(current_account)
end
def account_domain_block_scope
Account.not_domain_blocked_by_account(current_account)
end
end end

View file

@ -5,6 +5,7 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check
before_action :require_user_owned_by_application!, except: :check before_action :require_user_owned_by_application!, except: :check
before_action :require_user_not_confirmed!, except: :check before_action :require_user_not_confirmed!, except: :check
before_action :require_authenticated_user!, only: :check
def create def create
current_user.update!(email: params[:email]) if params.key?(:email) current_user.update!(email: params[:email]) if params.key?(:email)

View file

@ -21,7 +21,7 @@ class Api::V1::FavouritesController < Api::BaseController
end end
def results 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), limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id) params_slice(:max_id, :since_id, :min_id)
) )

View file

@ -7,7 +7,10 @@ class Api::V1::MarkersController < Api::BaseController
before_action :require_user! before_action :require_user!
def index 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) render json: serialize_map(@markers)
end end

View file

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

View file

@ -23,6 +23,6 @@ class Api::V1::ReportsController < Api::BaseController
end end
def report_params 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
end end

View file

@ -8,11 +8,15 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
def show def show
cache_if_unauthenticated! cache_if_unauthenticated!
render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer render json: status_edits, each_serializer: REST::StatusEditSerializer
end end
private private
def status_edits
@status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)]
end
def set_status def set_status
@status = Status.find(params[:status_id]) @status = Status.find(params[:status_id])
authorize @status, :show? authorize @status, :show?

View file

@ -6,11 +6,14 @@ class Api::V1::Timelines::HomeController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show def show
@statuses = load_statuses with_read_replica do
@statuses = load_statuses
@relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
render json: @statuses, render json: @statuses,
each_serializer: REST::StatusSerializer, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), relationships: @relationships,
status: account_home_feed.regenerating? ? 206 : 200 status: account_home_feed.regenerating? ? 206 : 200
end end

View file

@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController
private private
def next_path
api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end
def filtered_accounts def filtered_accounts
AccountFilter.new(translated_filter_params).results AccountFilter.new(translated_filter_params).results
end end

View file

@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController
params[:q], params[:q],
current_account, current_account,
limit_param(RESULTS_LIMIT), limit_param(RESULTS_LIMIT),
search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following))
) )
end end
def search_params def search_params
params.permit(:type, :offset, :min_id, :max_id, :account_id, :searchability) params.permit(:type, :offset, :min_id, :max_id, :account_id, :following, :searchability)
end end
end end

View file

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

View file

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

View file

@ -83,8 +83,10 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
def after_confirmation_path_for(_resource_name, user) def after_confirmation_path_for(_resource_name, user)
if user.created_by_application && truthy_param?(:redirect_to_app) if user.created_by_application && truthy_param?(:redirect_to_app)
user.created_by_application.confirmation_redirect_uri user.created_by_application.confirmation_redirect_uri
elsif user_signed_in?
web_url('start')
else else
super new_user_session_path
end end
end end
end end

View file

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

View file

@ -2,6 +2,7 @@
module CaptchaConcern module CaptchaConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Hcaptcha::Adapters::ViewMethods include Hcaptcha::Adapters::ViewMethods
included do included do
@ -35,18 +36,22 @@ module CaptchaConcern
flash.delete(:hcaptcha_error) flash.delete(:hcaptcha_error)
yield message yield message
end end
false false
end end
end end
def extend_csp_for_captcha! def extend_csp_for_captcha!
policy = request.content_security_policy policy = request.content_security_policy
return unless captcha_required? && policy.present? return unless captcha_required? && policy.present?
%w(script_src frame_src style_src connect_src).each do |directive| %w(script_src frame_src style_src connect_src).each do |directive|
values = policy.send(directive) values = policy.send(directive)
values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:') values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:')
values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:') values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:')
policy.send(directive, *values) policy.send(directive, *values)
end end
end end

View file

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

View file

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

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
class MailSubscriptionsController < ApplicationController
layout 'auth'
skip_before_action :require_functional!
before_action :set_body_classes
before_action :set_user
before_action :set_type
def show; end
def create
@user.settings[email_type_from_param] = false
@user.save!
end
private
def set_user
@user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe')
end
def set_body_classes
@body_classes = 'lighter'
end
def set_type
@type = email_type_from_param
end
def email_type_from_param
case params[:type]
when 'follow', 'reblog', 'favourite', 'mention', 'follow_request'
"notification_emails.#{params[:type]}"
else
raise ArgumentError
end
end
end

View file

@ -19,6 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController
end end
def user_params def user_params
params.require(:user).permit(:locale, chosen_languages: [], settings_attributes: UserSettings.keys) params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys)
end end
end end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class Settings::VerificationsController < Settings::BaseController
before_action :set_account
def show
@verified_links = @account.fields.select(&:verified?)
end
private
def set_account
@account = current_account
end
end

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 module DomainControlHelper
def domain_not_allowed?(uri_or_domain) 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?('://') domain = if uri_or_domain.include?('://')
Addressable::URI.parse(uri_or_domain).host Addressable::URI.parse(uri_or_domain).host

View file

@ -24,13 +24,4 @@ module SettingsHelper
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
end end
end end
def picture_hint(hint, picture)
if picture.original_filename.nil?
hint
else
link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete }
safe_join([hint, link], '<br/>'.html_safe)
end
end
end end

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View file

@ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR'; export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP'; export const ALERT_NOOP = 'ALERT_NOOP';
export function dismissAlert(alert) { export const dismissAlert = alert => ({
return { type: ALERT_DISMISS,
type: ALERT_DISMISS, alert,
alert, });
};
}
export function clearAlert() { export const clearAlert = () => ({
return { type: ALERT_CLEAR,
type: ALERT_CLEAR, });
};
}
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) { export const showAlert = alert => ({
return { type: ALERT_SHOW,
type: ALERT_SHOW, alert,
title, });
message,
message_values,
};
}
export function showAlertForError(error, skipNotFound = false) { export const showAlertForError = (error, skipNotFound = false) => {
if (error.response) { if (error.response) {
const { data, status, statusText, headers } = error.response; const { data, status, statusText, headers } = error.response;
// Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) { if (skipNotFound && (status === 404 || status === 410)) {
// Skip these errors as they are reflected in the UI
return { type: ALERT_NOOP }; return { type: ALERT_NOOP };
} }
// Rate limit errors
if (status === 429 && headers['x-ratelimit-reset']) { if (status === 429 && headers['x-ratelimit-reset']) {
const reset_date = new Date(headers['x-ratelimit-reset']); return showAlert({
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date }); title: messages.rateLimitedTitle,
message: messages.rateLimitedMessage,
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
});
} }
let message = statusText; return showAlert({
let title = `${status}`; title: `${status}`,
message: data.error || statusText,
if (data.error) { });
message = data.error;
}
return showAlert(title, message);
} else {
console.error(error);
return showAlert();
} }
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
} }

View file

@ -86,6 +86,8 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
const messages = defineMessages({ const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
open: { id: 'compose.published.open', defaultMessage: 'Open' },
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
}); });
export const ensureComposeIsVisible = (getState, routerHistory) => { export const ensureComposeIsVisible = (getState, routerHistory) => {
@ -133,13 +135,13 @@ export function resetCompose() {
}; };
} }
export const focusCompose = (routerHistory, defaultText) => dispatch => { export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
dispatch({ dispatch({
type: COMPOSE_FOCUS, type: COMPOSE_FOCUS,
defaultText, defaultText,
}); });
ensureComposeIsVisible(routerHistory); ensureComposeIsVisible(getState, routerHistory);
}; };
export function mentionCompose(account, routerHistory) { export function mentionCompose(account, routerHistory) {
@ -246,6 +248,13 @@ export function submitCompose(routerHistory) {
insertIfOnline('public'); insertIfOnline('public');
insertIfOnline(`account:${response.data.account.id}`); insertIfOnline(`account:${response.data.account.id}`);
} }
dispatch(showAlert({
message: messages.published,
action: messages.open,
dismissAfter: 10000,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));
}); });
@ -275,13 +284,14 @@ export function submitComposeFail(error) {
export function uploadCompose(files) { export function uploadCompose(files) {
return function (dispatch, getState) { return function (dispatch, getState) {
const uploadLimit = 4; const uploadLimit = 4;
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']); const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0); const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0); let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size + pending > uploadLimit) { if (files.length + media.size + pending > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit)); dispatch(showAlert({ message: messages.uploadErrorLimit }));
return; return;
} }

View file

@ -19,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU
export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL';
export const fetchServer = () => (dispatch, getState) => { export const fetchServer = () => (dispatch, getState) => {
if (getState().getIn(['server', 'server', 'isLoading'])) {
return;
}
dispatch(fetchServerRequest()); dispatch(fetchServerRequest());
api(getState) api(getState)
@ -66,6 +70,10 @@ const fetchServerTranslationLanguagesFail = error => ({
}); });
export const fetchExtendedDescription = () => (dispatch, getState) => { export const fetchExtendedDescription = () => (dispatch, getState) => {
if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) {
return;
}
dispatch(fetchExtendedDescriptionRequest()); dispatch(fetchExtendedDescriptionRequest());
api(getState) api(getState)
@ -89,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({
}); });
export const fetchDomainBlocks = () => (dispatch, getState) => { export const fetchDomainBlocks = () => (dispatch, getState) => {
if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) {
return;
}
dispatch(fetchDomainBlocksRequest()); dispatch(fetchDomainBlocksRequest());
api(getState) api(getState)

View file

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

View file

@ -1,6 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -8,15 +8,15 @@ import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { counterRenderer } from 'mastodon/components/common_counter';
import { EmptyAccount } from 'mastodon/components/empty_account'; import { EmptyAccount } from 'mastodon/components/empty_account';
import ShortNumber from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { me } from '../initial_state'; import { me } from '../initial_state';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import Button from './button'; import Button from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name'; import { DisplayName } from './display_name';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
import { RelativeTimestamp } from './relative_timestamp'; import { RelativeTimestamp } from './relative_timestamp';
@ -51,6 +51,7 @@ class Account extends ImmutablePureComponent {
defaultAction: PropTypes.string, defaultAction: PropTypes.string,
onActionClick: PropTypes.func, onActionClick: PropTypes.func,
children: PropTypes.object, children: PropTypes.object,
withBio: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -82,7 +83,7 @@ class Account extends ImmutablePureComponent {
}; };
render () { render () {
const { account, intl, hidden, hideButtons, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal, children } = this.props; const { account, intl, hidden, hideButtons, withBio, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal, children } = this.props;
if (!account) { if (!account) {
return <EmptyAccount size={size} minimal={minimal} />; return <EmptyAccount size={size} minimal={minimal} />;
@ -161,7 +162,7 @@ class Account extends ImmutablePureComponent {
<DisplayName account={account} /> <DisplayName account={account} />
{!minimal && ( {!minimal && (
<div className='account__details'> <div className='account__details'>
<ShortNumber value={account.get('followers_count')} isHide={account.getIn(['other_settings', 'hide_followers_count']) || false} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining} <ShortNumber value={account.get('followers_count')} isHide={account.getIn(['other_settings', 'hide_followers_count']) || false} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
</div> </div>
)} )}
</div> </div>
@ -178,6 +179,15 @@ class Account extends ImmutablePureComponent {
</div> </div>
)} )}
</div> </div>
{withBio && (account.get('note').length > 0 ? (
<div
className='account__note translate'
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
/>
) : (
<div className='account__note account__note--missing'><FormattedMessage id='account.no_bio' defaultMessage='No description provided.' /></div>
))}
</div> </div>
); );
} }

View file

@ -4,7 +4,7 @@ import { TransitionMotion, spring } from 'react-motion';
import { reduceMotion } from '../initial_state'; import { reduceMotion } from '../initial_state';
import ShortNumber from './short_number'; import { ShortNumber } from './short_number';
const obfuscatedCount = (count: number) => { const obfuscatedCount = (count: number) => {
if (count < 0) { if (count < 0) {
@ -32,7 +32,7 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]); const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
const willLeave = useCallback( const willLeave = useCallback(
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
[direction] [direction],
); );
if (reduceMotion) { if (reduceMotion) {

View file

@ -1,44 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import ShortNumber from 'mastodon/components/short_number';
export default class AutosuggestHashtag extends PureComponent {
static propTypes = {
tag: PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string,
history: PropTypes.array,
}).isRequired,
};
render() {
const { tag } = this.props;
const weeklyUses = tag.history && (
<ShortNumber
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
/>
);
return (
<div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>
#<strong>{tag.name}</strong>
</div>
{tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'>
<FormattedMessage
id='autosuggest_hashtag.per_week'
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/>
</div>
)}
</div>
);
}
}

View file

@ -0,0 +1,42 @@
import { FormattedMessage } from 'react-intl';
import { ShortNumber } from 'mastodon/components/short_number';
interface Props {
tag: {
name: string;
url?: string;
history?: {
uses: number;
accounts: string;
day: string;
}[];
following?: boolean;
type: 'hashtag';
};
}
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
const weeklyUses = tag.history && (
<ShortNumber
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
/>
);
return (
<div className='autosuggest-hashtag'>
<div className='autosuggest-hashtag__name'>
#<strong>{tag.name}</strong>
</div>
{tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'>
<FormattedMessage
id='autosuggest_hashtag.per_week'
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/>
</div>
)}
</div>
);
};

View file

@ -8,7 +8,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji'; import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag'; import { AutosuggestHashtag } from './autosuggest_hashtag';
const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
let word; let word;

View file

@ -10,7 +10,7 @@ import Textarea from 'react-textarea-autosize';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji'; import AutosuggestEmoji from './autosuggest_emoji';
import AutosuggestHashtag from './autosuggest_hashtag'; import { AutosuggestHashtag } from './autosuggest_hashtag';
const textAtCursorMatchesToken = (str, caretPosition) => { const textAtCursorMatchesToken = (str, caretPosition) => {
let word; let word;

View file

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

View file

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

View file

@ -0,0 +1,27 @@
interface Props {
size: number;
strokeWidth: number;
}
export const CircularProgress: React.FC<Props> = ({ size, strokeWidth }) => {
const viewBox = `0 0 ${size} ${size}`;
const radius = (size - strokeWidth) / 2;
return (
<svg
width={size}
height={size}
viewBox={viewBox}
className='circular-progress'
role='progressbar'
>
<circle
fill='none'
cx={size / 2}
cy={size / 2}
r={radius}
strokeWidth={`${strokeWidth}px`}
/>
</svg>
);
};

View file

@ -1,60 +0,0 @@
// @ts-check
import { FormattedMessage } from 'react-intl';
/**
* Returns custom renderer for one of the common counter types
* @param {"statuses" | "following" | "followers"} counterType
* Type of the counter
* @param {boolean} isBold Whether display number must be displayed in bold
* @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
* Renderer function
* @throws If counterType is not covered by this function
*/
export function counterRenderer(counterType, isBold = true) {
/**
* @type {(displayNumber: JSX.Element) => JSX.Element}
*/
const renderCounter = isBold
? (displayNumber) => <strong>{displayNumber}</strong>
: (displayNumber) => displayNumber;
switch (counterType) {
case 'statuses': {
return (displayNumber, pluralReady) => (
<FormattedMessage
id='account.statuses_counter'
defaultMessage='{count, plural, one {{counter} Post} other {{counter} Posts}}'
values={{
count: pluralReady,
counter: renderCounter(displayNumber),
}}
/>
);
}
case 'following': {
return (displayNumber, pluralReady) => (
<FormattedMessage
id='account.following_counter'
defaultMessage='{count, plural, one {{counter} Following} other {{counter} Following}}'
values={{
count: pluralReady,
counter: renderCounter(displayNumber),
}}
/>
);
}
case 'followers': {
return (displayNumber, pluralReady) => (
<FormattedMessage
id='account.followers_counter'
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
values={{
count: pluralReady,
counter: renderCounter(displayNumber),
}}
/>
);
}
default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`);
}
}

View file

@ -0,0 +1,45 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
export const StatusesCounter = (
displayNumber: React.ReactNode,
pluralReady: number,
) => (
<FormattedMessage
id='account.statuses_counter'
defaultMessage='{count, plural, one {{counter} Post} other {{counter} Posts}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
export const FollowingCounter = (
displayNumber: React.ReactNode,
pluralReady: number,
) => (
<FormattedMessage
id='account.following_counter'
defaultMessage='{count, plural, one {{counter} Following} other {{counter} Following}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
export const FollowersCounter = (
displayNumber: React.ReactNode,
pluralReady: number,
) => (
<FormattedMessage
id='account.followers_counter'
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);

View file

@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { injectIntl, defineMessages } from 'react-intl';
import { bannerSettings } from 'mastodon/settings';
import { IconButton } from './icon_button';
const messages = defineMessages({
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
});
class DismissableBanner extends PureComponent {
static propTypes = {
id: PropTypes.string.isRequired,
children: PropTypes.node,
intl: PropTypes.object.isRequired,
};
state = {
visible: !bannerSettings.get(this.props.id),
};
handleDismiss = () => {
const { id } = this.props;
this.setState({ visible: false }, () => bannerSettings.set(id, true));
};
render () {
const { visible } = this.state;
if (!visible) {
return null;
}
const { children, intl } = this.props;
return (
<div className='dismissable-banner'>
<div className='dismissable-banner__message'>
{children}
</div>
<div className='dismissable-banner__action'>
<IconButton icon='times' title={intl.formatMessage(messages.dismiss)} onClick={this.handleDismiss} />
</div>
</div>
);
}
}
export default injectIntl(DismissableBanner);

View file

@ -0,0 +1,47 @@
import type { PropsWithChildren } from 'react';
import { useCallback, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { bannerSettings } from 'mastodon/settings';
import { IconButton } from './icon_button';
const messages = defineMessages({
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
});
interface Props {
id: string;
}
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
id,
children,
}) => {
const [visible, setVisible] = useState(!bannerSettings.get(id));
const intl = useIntl();
const handleDismiss = useCallback(() => {
setVisible(false);
bannerSettings.set(id, true);
}, [id]);
if (!visible) {
return null;
}
return (
<div className='dismissable-banner'>
<div className='dismissable-banner__message'>{children}</div>
<div className='dismissable-banner__action'>
<IconButton
icon='times'
title={intl.formatMessage(messages.dismiss)}
onClick={handleDismiss}
/>
</div>
</div>
);
};

View file

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

View file

@ -8,8 +8,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay'; import Overlay from 'react-overlays/Overlay';
import { CircularProgress } from 'mastodon/components/loading_indicator'; import { CircularProgress } from "./circular_progress";
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;

View file

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

View file

@ -11,7 +11,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { Sparklines, SparklinesCurve } from 'react-sparklines';
import ShortNumber from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton'; import { Skeleton } from 'mastodon/components/skeleton';
class SilentErrorBoundary extends Component { class SilentErrorBoundary extends Component {

View file

@ -1,23 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
export default class LoadPending extends PureComponent {
static propTypes = {
onClick: PropTypes.func,
count: PropTypes.number,
};
render() {
const { count } = this.props;
return (
<button className='load-more load-gap' onClick={this.props.onClick}>
<FormattedMessage id='load_pending' defaultMessage='{count, plural, one {# new item} other {# new items}}' values={{ count }} />
</button>
);
}
}

View file

@ -0,0 +1,18 @@
import { FormattedMessage } from 'react-intl';
interface Props {
onClick: (event: React.MouseEvent) => void;
count: number;
}
export const LoadPending: React.FC<Props> = ({ onClick, count }) => {
return (
<button className='load-more load-gap' onClick={onClick}>
<FormattedMessage
id='load_pending'
defaultMessage='{count, plural, one {# new item} other {# new items}}'
values={{ count }}
/>
</button>
);
};

View file

@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
export const CircularProgress = ({ size, strokeWidth }) => {
const viewBox = `0 0 ${size} ${size}`;
const radius = (size - strokeWidth) / 2;
return (
<svg width={size} height={size} viewBox={viewBox} className='circular-progress' role='progressbar'>
<circle
fill='none'
cx={size / 2}
cy={size / 2}
r={radius}
strokeWidth={`${strokeWidth}px`}
/>
</svg>
);
};
CircularProgress.propTypes = {
size: PropTypes.number.isRequired,
strokeWidth: PropTypes.number.isRequired,
};
const LoadingIndicator = () => (
<div className='loading-indicator'>
<CircularProgress size={50} strokeWidth={6} />
</div>
);
export default LoadingIndicator;

View file

@ -0,0 +1,7 @@
import { CircularProgress } from './circular_progress';
export const LoadingIndicator: React.FC = () => (
<div className='loading-indicator'>
<CircularProgress size={50} strokeWidth={6} />
</div>
);

View file

@ -339,7 +339,10 @@ class MediaGallery extends PureComponent {
if (uncached) { if (uncached) {
spoilerButton = ( spoilerButton = (
<button type='button' disabled className='spoiler-button__overlay'> <button type='button' disabled className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'><FormattedMessage id='status.uncached_media_warning' defaultMessage='Not available' /></span> <span className='spoiler-button__overlay__label'>
<FormattedMessage id='status.uncached_media_warning' defaultMessage='Preview not available' />
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.open' defaultMessage='Click to open' /></span>
</span>
</button> </button>
); );
} else if (visible) { } else if (visible) {
@ -347,7 +350,10 @@ class MediaGallery extends PureComponent {
} else { } else {
spoilerButton = ( spoilerButton = (
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'> <button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
<span className='spoiler-button__overlay__label'>{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}</span> <span className='spoiler-button__overlay__label'>
{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
</span>
</button> </button>
); );
} }

View file

@ -130,6 +130,10 @@ class Poll extends ImmutablePureComponent {
this.props.refresh(); this.props.refresh();
}; };
handleReveal = () => {
this.setState({ revealed: true });
}
renderOption (option, optionIndex, showResults) { renderOption (option, optionIndex, showResults) {
const { poll, lang, disabled, intl } = this.props; const { poll, lang, disabled, intl } = this.props;
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
@ -205,14 +209,14 @@ class Poll extends ImmutablePureComponent {
render () { render () {
const { poll, intl } = this.props; const { poll, intl } = this.props;
const { expired } = this.state; const { revealed, expired } = this.state;
if (!poll) { if (!poll) {
return null; return null;
} }
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />; const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
const showResults = poll.get('voted') || expired; const showResults = poll.get('voted') || revealed || expired;
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
let votesCount = null; let votesCount = null;
@ -231,9 +235,10 @@ class Poll extends ImmutablePureComponent {
<div className='poll__footer'> <div className='poll__footer'>
{!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>} {!showResults && <button className='button button-secondary' disabled={disabled || !this.context.identity.signedIn} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
{showResults && !this.props.disabled && <span><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </span>} {!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
{showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
{votesCount} {votesCount}
{poll.get('expires_at') && <span> · {timeRemaining}</span>} {poll.get('expires_at') && <> · {timeRemaining}</>}
</div> </div>
</div> </div>
); );

View file

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

@ -16,8 +16,8 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { LoadMore } from './load_more'; import { LoadMore } from './load_more';
import LoadPending from './load_pending'; import { LoadPending } from './load_pending';
import LoadingIndicator from './loading_indicator'; import { LoadingIndicator } from './loading_indicator';
const MOUSE_IDLE_DELAY = 300; const MOUSE_IDLE_DELAY = 300;

View file

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { fetchServer } from 'mastodon/actions/server'; import { fetchServer } from 'mastodon/actions/server';
import { ServerHeroImage } from 'mastodon/components/server_hero_image'; import { ServerHeroImage } from 'mastodon/components/server_hero_image';
import ShortNumber from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton'; import { Skeleton } from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container'; import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state'; import { domain } from 'mastodon/initial_state';

View file

@ -1,116 +0,0 @@
import PropTypes from 'prop-types';
import { memo } from 'react';
import { FormattedMessage, FormattedNumber } from 'react-intl';
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
// @ts-check
/**
* @callback ShortNumberRenderer
* @param {JSX.Element} displayNumber Number to display
* @param {number} pluralReady Number used for pluralization
* @returns {JSX.Element} Final render of number
*/
/**
* @typedef {object} ShortNumberProps
* @property {number} value Number to display in short variant
* @property {ShortNumberRenderer} [renderer]
* Custom renderer for numbers, provided as a prop. If another renderer
* passed as a child of this component, this prop won't be used.
* @property {ShortNumberRenderer} [children]
* Custom renderer for numbers, provided as a child. If another renderer
* passed as a prop of this component, this one will be used instead.
*/
/**
* Component that renders short big number to a shorter version
* @param {ShortNumberProps} param0 Props for the component
* @returns {JSX.Element} Rendered number
*/
function ShortNumber({ value, isHide, renderer, children }) {
const shortNumber = toShortNumber(value);
const [, division] = shortNumber;
if (children != null && renderer != null) {
console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
}
const customRenderer = children != null ? children : renderer;
const displayNumber = !isHide ? <ShortNumberCounter value={shortNumber} /> : <span>-</span>;
return customRenderer != null
? customRenderer(displayNumber, pluralReady(value, division))
: displayNumber;
}
ShortNumber.propTypes = {
value: PropTypes.number.isRequired,
isHide: PropTypes.bool,
renderer: PropTypes.func,
children: PropTypes.func,
};
/**
* @typedef {object} ShortNumberCounterProps
* @property {import('../utils/number').ShortNumber} value Short number
*/
/**
* Renders short number into corresponding localizable react fragment
* @param {ShortNumberCounterProps} param0 Props for the component
* @returns {JSX.Element} FormattedMessage ready to be embedded in code
*/
function ShortNumberCounter({ value }) {
const [rawNumber, unit, maxFractionDigits = 0] = value;
const count = (
<FormattedNumber
value={rawNumber}
maximumFractionDigits={maxFractionDigits}
/>
);
let values = { count, rawNumber };
switch (unit) {
case DECIMAL_UNITS.THOUSAND: {
return (
<FormattedMessage
id='units.short.thousand'
defaultMessage='{count}K'
values={values}
/>
);
}
case DECIMAL_UNITS.MILLION: {
return (
<FormattedMessage
id='units.short.million'
defaultMessage='{count}M'
values={values}
/>
);
}
case DECIMAL_UNITS.BILLION: {
return (
<FormattedMessage
id='units.short.billion'
defaultMessage='{count}B'
values={values}
/>
);
}
// Not sure if we should go farther - @Sasha-Sorokin
default: return count;
}
}
ShortNumberCounter.propTypes = {
value: PropTypes.arrayOf(PropTypes.number),
};
export default memo(ShortNumber);

View file

@ -0,0 +1,92 @@
import { memo } from 'react';
import { FormattedMessage, FormattedNumber } from 'react-intl';
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
type ShortNumberRenderer = (
displayNumber: JSX.Element,
pluralReady: number,
) => JSX.Element;
interface ShortNumberProps {
value: number;
isHide: boolean;
renderer?: ShortNumberRenderer;
children?: ShortNumberRenderer;
}
export const ShortNumberRenderer: React.FC<ShortNumberProps> = ({
value,
isHide,
renderer,
children,
}) => {
const shortNumber = toShortNumber(value);
const [, division] = shortNumber;
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.',
);
}
const customRenderer = children ?? renderer ?? null;
const displayNumber = !isHide ? <ShortNumberCounter value={shortNumber} /> : <span>-</span>;
return (
customRenderer?.(displayNumber, pluralReady(value, division)) ??
displayNumber
);
};
export const ShortNumber = memo(ShortNumberRenderer);
interface ShortNumberCounterProps {
value: number[];
}
const ShortNumberCounter: React.FC<ShortNumberCounterProps> = ({ value }) => {
const [rawNumber, unit, maxFractionDigits = 0] = value;
const count = (
<FormattedNumber
value={rawNumber}
maximumFractionDigits={maxFractionDigits}
/>
);
const values = { count, rawNumber };
switch (unit) {
case DECIMAL_UNITS.THOUSAND: {
return (
<FormattedMessage
id='units.short.thousand'
defaultMessage='{count}K'
values={values}
/>
);
}
case DECIMAL_UNITS.MILLION: {
return (
<FormattedMessage
id='units.short.million'
defaultMessage='{count}M'
values={values}
/>
);
}
case DECIMAL_UNITS.BILLION: {
return (
<FormattedMessage
id='units.short.billion'
defaultMessage='{count}B'
values={values}
/>
);
}
// Not sure if we should go farther - @Sasha-Sorokin
default:
return count;
}
};

View file

@ -264,7 +264,6 @@ class StatusActionBar extends ImmutablePureComponent {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.context.identity; const { signedIn, permissions } = this.context.identity;
const anonymousAccess = !signedIn;
const publicStatus = ['public', 'unlisted', 'public_unlisted', 'login'].includes(status.get('visibility_ex')); const publicStatus = ['public', 'unlisted', 'public_unlisted', 'login'].includes(status.get('visibility_ex'));
const anonymousStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility_ex')); const anonymousStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility_ex'));
const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'login', 'private'].includes(status.get('visibility_ex')); const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'login', 'private'].includes(status.get('visibility_ex'));
@ -287,81 +286,83 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick }); menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
} }
if (anonymousStatus) { if (anonymousStatus && (signedIn || !isRemote)) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }
menu.push(null); if (signedIn) {
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancelReblog : messages.reblog), action: this.handleReblogForceModalClick });
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
}
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
if (writtenByMe && pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push(null);
if (writtenByMe || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
}
if (writtenByMe) {
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else {
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null); menu.push(null);
if (relationship && relationship.get('muting')) { menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancelReblog : messages.reblog), action: this.handleReblogForceModalClick });
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
if (publicStatus) {
menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
}
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
if (writtenByMe && pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
}
menu.push(null);
if (writtenByMe || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null);
}
if (writtenByMe) {
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true }); menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
} menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
if (relationship && relationship.get('blocking')) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
} else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
}
if (!this.props.onFilter) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
menu.push(null); menu.push(null);
if (relationship && relationship.get('domain_blocking')) { if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else { } else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true }); menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
} }
}
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { if (relationship && relationship.get('blocking')) {
menu.push(null); menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { } else {
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
} }
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
if (!this.props.onFilter) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
menu.push(null);
}
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1]; const domain = account.get('acct').split('@')[1];
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
menu.push(null);
if (relationship && relationship.get('domain_blocking')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
} else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
}
}
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null);
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
}
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
const domain = account.get('acct').split('@')[1];
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
}
} }
} }
} }
@ -410,7 +411,6 @@ class StatusActionBar extends ImmutablePureComponent {
<div className='status__action-bar__dropdown'> <div className='status__action-bar__dropdown'>
<DropdownMenuContainer <DropdownMenuContainer
scrollKey={scrollKey} scrollKey={scrollKey}
disabled={anonymousAccess}
status={status} status={status}
items={menu} items={menu}
icon='ellipsis-h' icon='ellipsis-h'

View file

@ -44,7 +44,7 @@ class TranslateButton extends PureComponent {
} }
return ( return (
<button className='status__content__read-more-button' onClick={onClick}> <button className='status__content__translate-button' onClick={onClick}>
<FormattedMessage id='status.translate' defaultMessage='Translate' /> <FormattedMessage id='status.translate' defaultMessage='Translate' />
</button> </button>
); );
@ -104,7 +104,7 @@ class StatusContent extends PureComponent {
if (mention) { if (mention) {
link.addEventListener('click', this.onMentionClick.bind(this, mention), false); link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
link.setAttribute('title', mention.get('acct')); link.setAttribute('title', `@${mention.get('acct')}`);
link.setAttribute('href', `/@${mention.get('acct')}`); link.setAttribute('href', `/@${mention.get('acct')}`);
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);

View file

@ -1,11 +1,27 @@
import { Icon } from './icon'; import { Icon } from './icon';
const domParser = new DOMParser();
const stripRelMe = (html: string) => {
const document = domParser.parseFromString(html, 'text/html').documentElement;
document.querySelectorAll<HTMLAnchorElement>('a[rel]').forEach((link) => {
link.rel = link.rel
.split(' ')
.filter((x: string) => x !== 'me')
.join(' ');
});
const body = document.querySelector('body');
return body ? { __html: body.innerHTML } : undefined;
};
interface Props { interface Props {
link: string; link: string;
} }
export const VerifiedBadge: React.FC<Props> = ({ link }) => ( export const VerifiedBadge: React.FC<Props> = ({ link }) => (
<span className='verified-badge'> <span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' /> <Icon id='check' className='verified-badge__mark' />
<span dangerouslySetInnerHTML={{ __html: link }} /> <span dangerouslySetInnerHTML={stripRelMe(link)} />
</span> </span>
); );

View file

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

View file

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

View file

@ -168,7 +168,7 @@ class About extends PureComponent {
</Section> </Section>
<Section title={intl.formatMessage(messages.rules)}> <Section title={intl.formatMessage(messages.rules)}>
{!isLoading && (server.get('rules').isEmpty() ? ( {!isLoading && (server.get('rules', []).isEmpty() ? (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
) : ( ) : (
<ol className='rules-list'> <ol className='rules-list'>

View file

@ -11,10 +11,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import Button from 'mastodon/components/button'; import Button from 'mastodon/components/button';
import { counterRenderer } from 'mastodon/components/common_counter'; import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import ShortNumber from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'mastodon/initial_state'; import { autoPlayGif, me, domain } from 'mastodon/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
@ -266,14 +266,14 @@ class Header extends ImmutablePureComponent {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = ''; actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) { } 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'])) { } 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'])) { } 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 { } 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'])) { if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
@ -292,7 +292,6 @@ class Header extends ImmutablePureComponent {
if (isRemote) { if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
menu.push(null);
} }
if ('share' in navigator) { if ('share' in navigator) {
@ -377,7 +376,7 @@ class Header extends ImmutablePureComponent {
let badge; let badge;
if (account.get('bot')) { if (account.get('bot')) {
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>); badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Automated' /></div>);
} else if (account.get('group')) { } else if (account.get('group')) {
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>); badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
} else { } else {
@ -455,7 +454,7 @@ class Header extends ImmutablePureComponent {
<ShortNumber <ShortNumber
value={account.get('statuses_count')} value={account.get('statuses_count')}
isHide={account.getIn(['other_settings', 'hide_statuses_count']) || false} isHide={account.getIn(['other_settings', 'hide_statuses_count']) || false}
renderer={counterRenderer('statuses')} renderer={StatusesCounter}
/> />
</NavLink> </NavLink>
@ -463,7 +462,7 @@ class Header extends ImmutablePureComponent {
<ShortNumber <ShortNumber
value={account.get('following_count')} value={account.get('following_count')}
isHide={account.getIn(['other_settings', 'hide_following_count']) || false} isHide={account.getIn(['other_settings', 'hide_following_count']) || false}
renderer={counterRenderer('following')} renderer={FollowingCounter}
/> />
</NavLink> </NavLink>
@ -471,7 +470,7 @@ class Header extends ImmutablePureComponent {
<ShortNumber <ShortNumber
value={account.get('followers_count')} value={account.get('followers_count')}
isHide={account.getIn(['other_settings', 'hide_followers_count']) || false} isHide={account.getIn(['other_settings', 'hide_followers_count']) || false}
renderer={counterRenderer('followers')} renderer={FollowersCounter}
/> />
</NavLink> </NavLink>
</div> </div>
@ -482,6 +481,7 @@ class Header extends ImmutablePureComponent {
<Helmet> <Helmet>
<title>{titleFromAccount(account)}</title> <title>{titleFromAccount(account)}</title>
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} /> <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
<link rel='canonical' href={account.get('url')} />
</Helmet> </Helmet>
</div> </div>
); );

View file

@ -10,7 +10,7 @@ import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import ColumnBackButton from 'mastodon/components/column_back_button'; import ColumnBackButton from 'mastodon/components/column_back_button';
import { LoadMore } from 'mastodon/components/load_more'; import { LoadMore } from 'mastodon/components/load_more';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map'; import { normalizeForLookup } from 'mastodon/reducers/accounts_map';

View file

@ -17,7 +17,7 @@ import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags'; import { fetchFeaturedTags } from '../../actions/featured_tags';
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines'; import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View file

@ -10,7 +10,7 @@ import { debounce } from 'lodash';
import { fetchBlocks, expandBlocks } from '../../actions/blocks'; import { fetchBlocks, expandBlocks } from '../../actions/blocks';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View file

@ -15,13 +15,14 @@ import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import StatusList from 'mastodon/components/status_list'; import StatusList from 'mastodon/components/status_list';
import Column from 'mastodon/features/ui/components/column'; import Column from 'mastodon/features/ui/components/column';
import { getStatusList } from 'mastodon/selectors';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']), statusIds: getStatusList(state, 'bookmarks'),
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
}); });

View file

@ -7,7 +7,7 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import DismissableBanner from 'mastodon/components/dismissable_banner'; import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import { domain } from 'mastodon/initial_state'; import { domain } from 'mastodon/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
@ -140,11 +140,8 @@ class CommunityTimeline extends PureComponent {
<ColumnSettingsContainer columnId={columnId} /> <ColumnSettingsContainer columnId={columnId} />
</ColumnHeader> </ColumnHeader>
<DismissableBanner id='community_timeline'>
<FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} />
</DismissableBanner>
<StatusListContainer <StatusListContainer
prepend={<DismissableBanner id='community_timeline'><FormattedMessage id='dismissable_banner.community_timeline' defaultMessage='These are the most recent public posts from people whose accounts are hosted by {domain}.' values={{ domain }} /></DismissableBanner>}
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`community_timeline-${columnId}`} scrollKey={`community_timeline-${columnId}`}
timelineId={`community${onlyMedia ? ':media' : ''}`} timelineId={`community${onlyMedia ? ':media' : ''}`}

View file

@ -66,7 +66,7 @@ class ActionBar extends PureComponent {
return ( return (
<div className='compose__action-bar'> <div className='compose__action-bar'>
<div className='compose__action-bar-dropdown'> <div className='compose__action-bar-dropdown'>
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={18} direction='right' /> <DropdownMenuContainer items={menu} icon='bars' size={18} direction='right' />
</div> </div>
</div> </div>
); );

View file

@ -408,7 +408,7 @@ class EmojiPickerDropdown extends PureComponent {
{button || <img {button || <img
className={classNames('emojione', { 'pulse-loading': active && loading })} className={classNames('emojione', { 'pulse-loading': active && loading })}
alt='🙂' alt='🙂'
src={`${assetHost}/emoji/1f602.svg`} src={`${assetHost}/emoji/1f642.svg`}
/>} />}
</div> </div>

View file

@ -46,7 +46,7 @@ export default class Upload extends ImmutablePureComponent {
const y = ((focusY / -2) + .5) * 100; const y = ((focusY / -2) + .5) * 100;
return ( return (
<div className='compose-form__upload' tabIndex={0} role='button'> <div className='compose-form__upload'>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => ( {({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>

View file

@ -19,7 +19,7 @@ import { openModal } from 'mastodon/actions/modal';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import Button from 'mastodon/components/button'; import Button from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name'; import { DisplayName } from 'mastodon/components/display_name';
import ShortNumber from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state'; import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
import { makeGetAccount } from 'mastodon/selectors'; import { makeGetAccount } from 'mastodon/selectors';
@ -160,16 +160,16 @@ class AccountCard extends ImmutablePureComponent {
if (!account.get('relationship')) { // Wait until the relationship is loaded if (!account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = ''; actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) { } 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'])) { } 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'])) { } 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'])) { } 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 { } 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 ( return (

View file

@ -14,7 +14,7 @@ import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import { LoadMore } from 'mastodon/components/load_more'; import { LoadMore } from 'mastodon/components/load_more';
import LoadingIndicator from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RadioButton } from 'mastodon/components/radio_button'; import { RadioButton } from 'mastodon/components/radio_button';
import ScrollContainer from 'mastodon/containers/scroll_container'; import ScrollContainer from 'mastodon/containers/scroll_container';

View file

@ -12,7 +12,7 @@ import { debounce } from 'lodash';
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import LoadingIndicator from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import DomainContainer from '../../containers/domain_container'; import DomainContainer from '../../containers/domain_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View file

@ -0,0 +1,52 @@
import type { BaseEmoji, EmojiData, NimbleEmojiIndex } from 'emoji-mart';
import type { Category, Data, Emoji } from 'emoji-mart/dist-es/utils/data';
/*
* The 'search' property, although not defined in the [`Emoji`]{@link node_modules/@types/emoji-mart/dist-es/utils/data.d.ts#Emoji} type,
* is used in the application.
* This could be due to an oversight by the library maintainer.
* The `search` property is defined and used [here]{@link node_modules/emoji-mart/dist/utils/data.js#uncompress}.
*/
export type Search = string;
/*
* The 'skins' property does not exist in the application data.
* This could be a potential area of refactoring or error handling.
* The non-existence of 'skins' property is evident at [this location]{@link app/javascript/mastodon/features/emoji/emoji_compressed.js:121}.
*/
export type Skins = null;
export type FilenameData = string[] | string[][];
export type ShortCodesToEmojiDataKey =
| EmojiData['id']
| BaseEmoji['native']
| keyof NimbleEmojiIndex['emojis'];
export type SearchData = [
BaseEmoji['native'],
Emoji['short_names'],
Search,
Emoji['unified'],
];
export type ShortCodesToEmojiData = Record<
ShortCodesToEmojiDataKey,
[FilenameData, SearchData]
>;
export type EmojisWithoutShortCodes = FilenameData[];
export type EmojiCompressed = [
ShortCodesToEmojiData,
Skins,
Category[],
Data['aliases'],
EmojisWithoutShortCodes,
];
/*
* `emoji_compressed.js` uses `babel-plugin-preval`, which makes it difficult to convert to TypeScript.
* As a temporary solution, we are allowing a default export here to apply the TypeScript type `EmojiCompressed` to the JS file export.
* - {@link app/javascript/mastodon/features/emoji/emoji_compressed.js}
*/
declare const emojiCompressed: EmojiCompressed;
export default emojiCompressed; // eslint-disable-line import/no-default-export

View file

@ -118,6 +118,16 @@ Object.keys(emojiIndex.emojis).forEach(key => {
// inconsistent behavior in dev mode // inconsistent behavior in dev mode
module.exports = JSON.parse(JSON.stringify([ module.exports = JSON.parse(JSON.stringify([
shortCodesToEmojiData, shortCodesToEmojiData,
/*
* The property `skins` is not found in the current context.
* This could potentially lead to issues when interacting with modules or data structures
* that expect the presence of `skins` property.
* Currently, no definitions or references to `skins` property can be found in:
* - {@link node_modules/emoji-mart/dist/utils/data.js}
* - {@link node_modules/emoji-mart/data/all.json}
* - {@link app/javascript/mastodon/features/emoji/emoji_compressed.d.ts#Skins}
* Future refactorings or updates should consider adding definitions or handling for `skins` property.
*/
emojiMartData.skins, emojiMartData.skins,
emojiMartData.categories, emojiMartData.categories,
emojiMartData.aliases, emojiMartData.aliases,

View file

@ -1,43 +0,0 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;
const emojis = {};
// decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
let [
filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
searchData,
] = shortCodesToEmojiData[shortCode];
let [
native,
short_names,
search,
unified,
] = searchData;
if (!unified) {
// unified name can be derived from unicodeToUnifiedName
unified = unicodeToUnifiedName(native);
}
short_names = [shortCode].concat(short_names);
emojis[shortCode] = {
native,
search,
short_names,
unified,
};
});
export {
emojis,
skins,
categories,
short_names,
};

View file

@ -0,0 +1,52 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
import type { BaseEmoji } from 'emoji-mart';
import type { Emoji } from 'emoji-mart/dist-es/utils/data';
import type { Search, ShortCodesToEmojiData } from './emoji_compressed';
import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
type Emojis = {
[key in NonNullable<keyof ShortCodesToEmojiData>]: {
native: BaseEmoji['native'];
search: Search;
short_names: Emoji['short_names'];
unified: Emoji['unified'];
};
};
const [
shortCodesToEmojiData,
skins,
categories,
short_names,
_emojisWithoutShortCodes,
] = emojiCompressed;
const emojis: Emojis = {};
// decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
const [_filenameData, searchData] = shortCodesToEmojiData[shortCode];
const native = searchData[0];
let short_names = searchData[1];
const search = searchData[2];
let unified = searchData[3];
if (!unified) {
// unified name can be derived from unicodeToUnifiedName
unified = unicodeToUnifiedName(native);
}
if (short_names) short_names = [shortCode].concat(short_names);
emojis[shortCode] = {
native,
search,
short_names,
unified,
};
});
export { emojis, skins, categories, short_names };

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