Merge pull request #2291 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes
This commit is contained in:
Claire 2023-07-12 22:17:29 +02:00 committed by GitHub
commit bb61faa64b
575 changed files with 2712 additions and 2331 deletions

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/**/*'
@ -192,6 +191,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

@ -48,15 +48,6 @@ Layout/SpaceInLambdaLiteral:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb' - 'config/initializers/content_security_policy.rb'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowedMethods, AllowedPatterns.
Lint/AmbiguousBlockAssociation:
Exclude:
- 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb'
- 'spec/services/activitypub/process_status_update_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
# Configuration parameters: AllowComments, AllowEmptyLambdas. # Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock: Lint/EmptyBlock:
Exclude: Exclude:
@ -106,11 +97,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:
@ -167,10 +153,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 27 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
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
@ -178,19 +160,6 @@ Naming/FileName:
Exclude: Exclude:
- 'config/locales/sr-Latn.rb' - 'config/locales/sr-Latn.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyleForLeadingUnderscores.
# SupportedStylesForLeadingUnderscores: disallowed, required, optional
Naming/MemoizedInstanceVariableName:
Exclude:
- 'app/controllers/api/v1/bookmarks_controller.rb'
- 'app/controllers/api/v1/favourites_controller.rb'
- 'app/controllers/concerns/rate_limit_headers.rb'
- 'app/lib/activitypub/activity.rb'
- 'app/services/resolve_url_service.rb'
- 'app/services/search_service.rb'
- 'config/initializers/rack_attack.rb'
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # 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
@ -400,45 +369,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/models/account_migration_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/relationship_filter_spec.rb'
- 'spec/models/user_role_spec.rb'
- 'spec/policies/account_moderation_note_policy_spec.rb'
- 'spec/policies/account_policy_spec.rb'
- 'spec/policies/backup_policy_spec.rb'
- 'spec/policies/custom_emoji_policy_spec.rb'
- 'spec/policies/domain_block_policy_spec.rb'
- 'spec/policies/email_domain_block_policy_spec.rb'
- 'spec/policies/instance_policy_spec.rb'
- 'spec/policies/invite_policy_spec.rb'
- 'spec/policies/relay_policy_spec.rb'
- 'spec/policies/report_note_policy_spec.rb'
- 'spec/policies/report_policy_spec.rb'
- 'spec/policies/settings_policy_spec.rb'
- 'spec/policies/tag_policy_spec.rb'
- 'spec/policies/user_policy_spec.rb'
- 'spec/services/activitypub/process_account_service_spec.rb'
RSpec/SubjectStub:
Exclude:
- 'spec/services/unallow_domain_service_spec.rb'
- 'spec/validators/blacklisted_email_validator_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Rails/ApplicationController: Rails/ApplicationController:
Exclude: Exclude:
@ -779,406 +709,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:
@ -1340,13 +870,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
@ -1360,21 +883,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

@ -11,7 +11,6 @@ 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'
@ -159,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

@ -130,7 +130,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 +146,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)
@ -291,11 +291,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)
@ -399,8 +399,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)
@ -670,7 +668,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)
@ -767,7 +765,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)
@ -815,7 +813,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)

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

@ -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

@ -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

@ -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

@ -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
ApplicationRecord.connected_to(role: :read, prevent_writes: true) do
@statuses = load_statuses @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

@ -129,7 +129,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

@ -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)
use_pack 'auth' use_pack 'auth'

View file

@ -22,7 +22,7 @@ module AccountsHelper
def account_action_button(account) def account_action_button(account)
return if account.memorial? || account.moved? return if account.memorial? || account.moved?
link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do link_to ActivityPub::TagManager.instance.url_for(account), class: 'button', target: '_new' do
safe_join([logo_as_symbol, t('accounts.follow')]) safe_join([logo_as_symbol, t('accounts.follow')])
end end
end end

View file

@ -5,7 +5,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) {

View file

@ -1,6 +1,6 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ShortNumber from 'flavours/glitch/components/short_number'; import { ShortNumber } from 'flavours/glitch/components/short_number';
interface Props { interface Props {
tag: { tag: {

View file

@ -1,59 +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 'flavours/glitch/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 'flavours/glitch/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

@ -10,7 +10,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { Sparklines, SparklinesCurve } from 'react-sparklines'; import { Sparklines, SparklinesCurve } from 'react-sparklines';
import ShortNumber from 'flavours/glitch/components/short_number'; import { ShortNumber } from 'flavours/glitch/components/short_number';
import { Skeleton } from 'flavours/glitch/components/skeleton'; import { Skeleton } from 'flavours/glitch/components/skeleton';
import Permalink from './permalink'; import Permalink from './permalink';

View file

@ -354,7 +354,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) {
@ -362,7 +365,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

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

View file

@ -1,114 +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, 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 = <ShortNumberCounter value={shortNumber} />;
return customRenderer != null
? customRenderer(displayNumber, pluralReady(value, division))
: displayNumber;
}
ShortNumber.propTypes = {
value: PropTypes.number.isRequired,
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,90 @@
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;
renderer?: ShortNumberRenderer;
children?: ShortNumberRenderer;
}
export const ShortNumberRenderer: React.FC<ShortNumberProps> = ({
value,
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 = <ShortNumberCounter value={shortNumber} />;
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

@ -97,7 +97,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>
); );

View file

@ -196,14 +196,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 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 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 if (profileLink) { } else if (profileLink) {
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'])) {

View file

@ -12,7 +12,7 @@ import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column'; import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header'; import ColumnHeader from 'flavours/glitch/components/column_header';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import { domain } from 'flavours/glitch/initial_state'; import { domain } from 'flavours/glitch/initial_state';

View file

@ -20,7 +20,7 @@ import Button from 'flavours/glitch/components/button';
import { DisplayName } from 'flavours/glitch/components/display_name'; import { DisplayName } from 'flavours/glitch/components/display_name';
import { IconButton } from 'flavours/glitch/components/icon_button'; import { IconButton } from 'flavours/glitch/components/icon_button';
import Permalink from 'flavours/glitch/components/permalink'; import Permalink from 'flavours/glitch/components/permalink';
import ShortNumber from 'flavours/glitch/components/short_number'; import { ShortNumber } from 'flavours/glitch/components/short_number';
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/initial_state'; import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/initial_state';
import { makeGetAccount } from 'flavours/glitch/selectors'; import { makeGetAccount } from 'flavours/glitch/selectors';
@ -171,16 +171,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

@ -5,7 +5,7 @@ import classNames from 'classnames';
import { Blurhash } from 'flavours/glitch/components/blurhash'; import { Blurhash } from 'flavours/glitch/components/blurhash';
import { accountsCountRenderer } from 'flavours/glitch/components/hashtag'; import { accountsCountRenderer } from 'flavours/glitch/components/hashtag';
import ShortNumber from 'flavours/glitch/components/short_number'; import { ShortNumber } from 'flavours/glitch/components/short_number';
import { Skeleton } from 'flavours/glitch/components/skeleton'; import { Skeleton } from 'flavours/glitch/components/skeleton';

View file

@ -11,7 +11,7 @@ import { connect } from 'react-redux';
import Column from 'flavours/glitch/components/column'; import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header'; import ColumnHeader from 'flavours/glitch/components/column_header';
import Search from 'flavours/glitch/features/compose/containers/search_container'; import Search from 'flavours/glitch/features/compose/containers/search_container';
import { showTrends } from 'flavours/glitch/initial_state'; import { trendsEnabled } from 'flavours/glitch/initial_state';
import Links from './links'; import Links from './links';
import SearchResults from './results'; import SearchResults from './results';
@ -28,7 +28,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
layout: state.getIn(['meta', 'layout']), layout: state.getIn(['meta', 'layout']),
isSearching: state.getIn(['search', 'submitted']) || !showTrends, isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
}); });
class Explore extends PureComponent { class Explore extends PureComponent {

View file

@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchTrendingLinks } from 'flavours/glitch/actions/trends'; import { fetchTrendingLinks } from 'flavours/glitch/actions/trends';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import Story from './components/story'; import Story from './components/story';

View file

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends'; import { fetchTrendingStatuses, expandTrendingStatuses } from 'flavours/glitch/actions/trends';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import StatusList from 'flavours/glitch/components/status_list'; import StatusList from 'flavours/glitch/components/status_list';
import { getStatusList } from 'flavours/glitch/selectors'; import { getStatusList } from 'flavours/glitch/selectors';
@ -52,6 +52,7 @@ class Statuses extends PureComponent {
<StatusList <StatusList
trackScroll trackScroll
timelineId='explore'
statusIds={statusIds} statusIds={statusIds}
scrollKey='explore-statuses' scrollKey='explore-statuses'
hasMore={hasMore} hasMore={hasMore}

View file

@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends'; import { fetchTrendingHashtags } from 'flavours/glitch/actions/trends';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';

View file

@ -10,7 +10,7 @@ import { addColumn } from 'flavours/glitch/actions/columns';
import { changeSetting } from 'flavours/glitch/actions/settings'; import { changeSetting } from 'flavours/glitch/actions/settings';
import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming'; import { connectPublicStream, connectCommunityStream } from 'flavours/glitch/actions/streaming';
import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import SettingText from 'flavours/glitch/components/setting_text'; import SettingText from 'flavours/glitch/components/setting_text';
import initialState, { domain } from 'flavours/glitch/initial_state'; import initialState, { domain } from 'flavours/glitch/initial_state';
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';

View file

@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import background from 'mastodon/../images/friends-cropped.png'; import background from 'mastodon/../images/friends-cropped.png';

View file

@ -12,7 +12,7 @@ import { connectPublicStream } from 'flavours/glitch/actions/streaming';
import { expandPublicTimeline } from 'flavours/glitch/actions/timelines'; import { expandPublicTimeline } from 'flavours/glitch/actions/timelines';
import Column from 'flavours/glitch/components/column'; import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header'; import ColumnHeader from 'flavours/glitch/components/column_header';
import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
import { domain } from 'flavours/glitch/initial_state'; import { domain } from 'flavours/glitch/initial_state';

View file

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

View file

@ -4,7 +4,7 @@ import { Component } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import NavigationPortal from 'flavours/glitch/components/navigation_portal'; import NavigationPortal from 'flavours/glitch/components/navigation_portal';
import { timelinePreview, showTrends } from 'flavours/glitch/initial_state'; import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state';
import { preferencesLink } from 'flavours/glitch/utils/backend_links'; import { preferencesLink } from 'flavours/glitch/utils/backend_links';
import ColumnLink from './column_link'; import ColumnLink from './column_link';
@ -60,7 +60,7 @@ class NavigationPanel extends Component {
</> </>
)} )}
{showTrends ? ( {trendsEnabled ? (
<ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} /> <ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
) : ( ) : (
<ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} /> <ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />

View file

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

View file

@ -23,7 +23,7 @@ import PermaLink from 'flavours/glitch/components/permalink';
import PictureInPicture from 'flavours/glitch/features/picture_in_picture'; import PictureInPicture from 'flavours/glitch/features/picture_in_picture';
import { layoutFromWindow } from 'flavours/glitch/is_mobile'; import { layoutFromWindow } from 'flavours/glitch/is_mobile';
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state'; import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding } from '../../initial_state';
import BundleColumnError from './components/bundle_column_error'; import BundleColumnError from './components/bundle_column_error';
import Header from './components/header'; import Header from './components/header';
@ -178,7 +178,7 @@ class SwitchingColumnsArea extends PureComponent {
} }
} else if (singleUserMode && owner && initialState?.accounts[owner]) { } else if (singleUserMode && owner && initialState?.accounts[owner]) {
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />; redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
} else if (showTrends && trendsAsLanding) { } else if (trendsEnabled && trendsAsLanding) {
redirect = <Redirect from='/' to='/explore' exact />; redirect = <Redirect from='/' to='/explore' exact />;
} else { } else {
redirect = <Redirect from='/' to='/about' exact />; redirect = <Redirect from='/' to='/about' exact />;

View file

@ -70,12 +70,13 @@
* @property {boolean} reduce_motion * @property {boolean} reduce_motion
* @property {string} repository * @property {string} repository
* @property {boolean} search_enabled * @property {boolean} search_enabled
* @property {boolean} trends_enabled
* @property {boolean} single_user_mode * @property {boolean} single_user_mode
* @property {string} source_url * @property {string} source_url
* @property {string} streaming_api_base_url * @property {string} streaming_api_base_url
* @property {boolean} timeline_preview * @property {boolean} timeline_preview
* @property {string} title * @property {string} title
* @property {boolean} trends * @property {boolean} show_trends
* @property {boolean} trends_as_landing_page * @property {boolean} trends_as_landing_page
* @property {boolean} unfollow_modal * @property {boolean} unfollow_modal
* @property {boolean} use_blurhash * @property {boolean} use_blurhash
@ -139,7 +140,8 @@ export const reduceMotion = getMeta('reduce_motion');
export const registrationsOpen = getMeta('registrations_open'); export const registrationsOpen = getMeta('registrations_open');
export const repository = getMeta('repository'); export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled'); export const searchEnabled = getMeta('search_enabled');
export const showTrends = getMeta('trends'); export const trendsEnabled = getMeta('trends_enabled');
export const showTrends = getMeta('show_trends');
export const singleUserMode = getMeta('single_user_mode'); export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url'); export const source_url = getMeta('source_url');
export const timelinePreview = getMeta('timeline_preview'); export const timelinePreview = getMeta('timeline_preview');

View file

@ -1,5 +1,8 @@
import type { Middleware, AnyAction } from 'redux'; import type { Middleware, AnyAction } from 'redux';
import ready from 'flavours/glitch/ready';
import { assetHost } from 'flavours/glitch/utils/config';
import type { RootState } from '..'; import type { RootState } from '..';
interface AudioSource { interface AudioSource {
@ -35,18 +38,20 @@ export const soundsMiddleware = (): Middleware<
Record<string, never>, Record<string, never>,
RootState RootState
> => { > => {
const soundCache: { [key: string]: HTMLAudioElement } = { const soundCache: { [key: string]: HTMLAudioElement } = {};
boop: createAudio([
void ready(() => {
soundCache.boop = createAudio([
{ {
src: '/sounds/boop.ogg', src: `${assetHost}/sounds/boop.ogg`,
type: 'audio/ogg', type: 'audio/ogg',
}, },
{ {
src: '/sounds/boop.mp3', src: `${assetHost}/sounds/boop.mp3`,
type: 'audio/mpeg', type: 'audio/mpeg',
}, },
]), ]);
}; });
return () => return () =>
(next) => (next) =>

View file

@ -150,7 +150,6 @@
.layout-multiple-columns &.button--with-bell { .layout-multiple-columns &.button--with-bell {
font-size: 12px; font-size: 12px;
padding: 0 8px;
} }
} }
@ -1343,34 +1342,30 @@ button.icon-button.active i.fa-retweet {
} }
&__overlay { &__overlay {
display: block; display: flex;
background: transparent; align-items: center;
justify-content: center;
background: rgba($black, 0.5);
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0;
margin: 0;
border: 0; border: 0;
&__label { &__label {
display: inline-block; display: flex;
background: rgba($base-overlay-background, 0.5); align-items: center;
border-radius: 8px; justify-content: center;
padding: 8px 12px; gap: 8px;
flex-direction: column;
color: $primary-text-color; color: $primary-text-color;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
} }
&:hover, &__action {
&:focus, font-weight: 400;
&:active { font-size: 13px;
.spoiler-button__overlay__label {
background: rgba($base-overlay-background, 0.8);
}
}
&:disabled {
.spoiler-button__overlay__label {
background: rgba($base-overlay-background, 0.5);
}
} }
} }
} }

View file

@ -709,6 +709,7 @@
&__toggle { &__toggle {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 10px;
& > span { & > span {
font-size: 17px; font-size: 17px;

View file

@ -657,10 +657,6 @@ a.status__display-name,
color: inherit; color: inherit;
} }
.detailed-status .button.logo-button {
margin-bottom: 15px;
}
.detailed-status__display-name { .detailed-status__display-name {
color: $secondary-text-color; color: $secondary-text-color;
display: block; display: block;

View file

@ -15,7 +15,8 @@
.status__content a, .status__content a,
.link-footer a, .link-footer a,
.reply-indicator__content a, .reply-indicator__content a,
.status__content__read-more-button { .status__content__read-more-button,
.status__content__translate-button {
text-decoration: underline; text-decoration: underline;
&:hover, &:hover,

View file

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

View file

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

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,
title, alert,
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;
if (skipNotFound && (status === 404 || status === 410)) {
// Skip these errors as they are reflected in the UI // Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
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); console.error(error);
return showAlert();
} return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
} }

View file

@ -82,6 +82,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) => {
@ -242,6 +244,13 @@ export function submitCompose(routerHistory) {
} }
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));
}); });
@ -274,15 +283,16 @@ export function uploadCompose(files) {
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;
} }
if (getState().getIn(['compose', 'poll'])) { if (getState().getIn(['compose', 'poll'])) {
dispatch(showAlert(undefined, messages.uploadErrorPoll)); dispatch(showAlert({ message: messages.uploadErrorPoll }));
return; return;
} }

View file

@ -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';
@ -160,7 +160,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')} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining} <ShortNumber value={account.get('followers_count')} renderer={FollowersCounter} /> {verification} {muteTimeRemaining}
</div> </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) {

View file

@ -1,6 +1,6 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import ShortNumber from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
interface Props { interface Props {
tag: { tag: {

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

@ -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

@ -321,7 +321,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) {
@ -329,7 +332,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

@ -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,115 +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, 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 = <ShortNumberCounter value={shortNumber} />;
return customRenderer != null
? customRenderer(displayNumber, pluralReady(value, division))
: displayNumber;
}
ShortNumber.propTypes = {
value: PropTypes.number.isRequired,
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,90 @@
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;
renderer?: ShortNumberRenderer;
children?: ShortNumberRenderer;
}
export const ShortNumberRenderer: React.FC<ShortNumberProps> = ({
value,
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 = <ShortNumberCounter value={shortNumber} />;
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

@ -237,7 +237,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'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
@ -263,6 +262,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }
if (signedIn) {
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
@ -331,6 +331,7 @@ class StatusActionBar extends ImmutablePureComponent {
} }
} }
} }
}
let replyIcon; let replyIcon;
let replyTitle; let replyTitle;
@ -371,7 +372,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>
); );

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';
@ -264,14 +264,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'])) {
@ -290,7 +290,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) {
@ -451,21 +450,21 @@ class Header extends ImmutablePureComponent {
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}> <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}>
<ShortNumber <ShortNumber
value={account.get('statuses_count')} value={account.get('statuses_count')}
renderer={counterRenderer('statuses')} renderer={StatusesCounter}
/> />
</NavLink> </NavLink>
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}> <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}>
<ShortNumber <ShortNumber
value={account.get('following_count')} value={account.get('following_count')}
renderer={counterRenderer('following')} renderer={FollowingCounter}
/> />
</NavLink> </NavLink>
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> <NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
<ShortNumber <ShortNumber
value={account.get('followers_count')} value={account.get('followers_count')}
renderer={counterRenderer('followers')} renderer={FollowersCounter}
/> />
</NavLink> </NavLink>
</div> </div>

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';

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

@ -5,7 +5,7 @@ import classNames from 'classnames';
import { Blurhash } from 'mastodon/components/blurhash'; import { Blurhash } from 'mastodon/components/blurhash';
import { accountsCountRenderer } from 'mastodon/components/hashtag'; import { accountsCountRenderer } from 'mastodon/components/hashtag';
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';
export default class Story extends PureComponent { export default class Story extends PureComponent {

View file

@ -11,7 +11,7 @@ import { connect } from 'react-redux';
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 Search from 'mastodon/features/compose/containers/search_container'; import Search from 'mastodon/features/compose/containers/search_container';
import { showTrends } from 'mastodon/initial_state'; import { trendsEnabled } from 'mastodon/initial_state';
import Links from './links'; import Links from './links';
import SearchResults from './results'; import SearchResults from './results';
@ -26,7 +26,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
layout: state.getIn(['meta', 'layout']), layout: state.getIn(['meta', 'layout']),
isSearching: state.getIn(['search', 'submitted']) || !showTrends, isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
}); });
class Explore extends PureComponent { class Explore extends PureComponent {

View file

@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchTrendingLinks } from 'mastodon/actions/trends'; import { fetchTrendingLinks } from 'mastodon/actions/trends';
import DismissableBanner from 'mastodon/components/dismissable_banner'; import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import Story from './components/story'; import Story from './components/story';

View file

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends'; import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
import DismissableBanner from 'mastodon/components/dismissable_banner'; import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import StatusList from 'mastodon/components/status_list'; import StatusList from 'mastodon/components/status_list';
import { getStatusList } from 'mastodon/selectors'; import { getStatusList } from 'mastodon/selectors';
@ -52,6 +52,7 @@ class Statuses extends PureComponent {
<StatusList <StatusList
trackScroll trackScroll
timelineId='explore'
statusIds={statusIds} statusIds={statusIds}
scrollKey='explore-statuses' scrollKey='explore-statuses'
hasMore={hasMore} hasMore={hasMore}

View file

@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { fetchTrendingHashtags } from 'mastodon/actions/trends'; import { fetchTrendingHashtags } from 'mastodon/actions/trends';
import DismissableBanner from 'mastodon/components/dismissable_banner'; import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';

View file

@ -10,7 +10,7 @@ import { addColumn } from 'mastodon/actions/columns';
import { changeSetting } from 'mastodon/actions/settings'; import { changeSetting } from 'mastodon/actions/settings';
import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming'; import { connectPublicStream, connectCommunityStream } from 'mastodon/actions/streaming';
import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines'; import { expandPublicTimeline, expandCommunityTimeline } from 'mastodon/actions/timelines';
import DismissableBanner from 'mastodon/components/dismissable_banner'; import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import initialState, { domain } from 'mastodon/initial_state'; import initialState, { domain } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';

View file

@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import background from 'mastodon/../images/friends-cropped.png'; import background from 'mastodon/../images/friends-cropped.png';
import DismissableBanner from 'mastodon/components/dismissable_banner'; import { DismissableBanner } from 'mastodon/components/dismissable_banner';
export const ExplorePrompt = () => ( export const ExplorePrompt = () => (

View file

@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === 'granted') { if (permission === 'granted') {
dispatch(changePushNotifications(path.slice(1), checked)); dispatch(changePushNotifications(path.slice(1), checked));
} else { } else {
dispatch(showAlert(undefined, messages.permissionDenied)); dispatch(showAlert({ message: messages.permissionDenied }));
} }
})); }));
} else { } else {
@ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === 'granted') { if (permission === 'granted') {
dispatch(changeSetting(['notifications', ...path], checked)); dispatch(changeSetting(['notifications', ...path], checked));
} else { } else {
dispatch(showAlert(undefined, messages.permissionDenied)); dispatch(showAlert({ message: messages.permissionDenied }));
} }
})); }));
} else { } else {

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';

View file

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

View file

@ -195,21 +195,23 @@ class ActionBar extends PureComponent {
let menu = []; let menu = [];
if (publicStatus) { if (publicStatus && isRemote) {
if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
} }
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
if ('share' in navigator) { if (publicStatus && 'share' in navigator) {
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare }); menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
} }
if (publicStatus) {
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(null);
if (writtenByMe) { if (writtenByMe) {
if (pinnableStatus) { if (pinnableStatus) {
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
@ -263,6 +265,7 @@ class ActionBar extends PureComponent {
} }
} }
} }
}
let replyIcon; let replyIcon;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
@ -292,7 +295,7 @@ class ActionBar extends PureComponent {
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' disabled={!signedIn} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> <DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
</div> </div>
</div> </div>
); );

View file

@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
import { WordmarkLogo } from 'mastodon/components/logo'; import { WordmarkLogo } from 'mastodon/components/logo';
import NavigationPortal from 'mastodon/components/navigation_portal'; import NavigationPortal from 'mastodon/components/navigation_portal';
import { timelinePreview, showTrends } from 'mastodon/initial_state'; import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
import ColumnLink from './column_link'; import ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner'; import DisabledAccountBanner from './disabled_account_banner';
@ -65,7 +65,7 @@ class NavigationPanel extends Component {
</> </>
)} )}
{showTrends ? ( {trendsEnabled ? (
<ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} /> <ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
) : ( ) : (
<ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} /> <ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />

View file

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

View file

@ -7,26 +7,27 @@ import { NotificationStack } from 'react-notification';
import { dismissAlert } from '../../../actions/alerts'; import { dismissAlert } from '../../../actions/alerts';
import { getAlerts } from '../../../selectors'; import { getAlerts } from '../../../selectors';
const mapStateToProps = (state, { intl }) => { const formatIfNeeded = (intl, message, values) => {
const notifications = getAlerts(state); if (typeof message === 'object') {
return intl.formatMessage(message, values);
notifications.forEach(notification => ['title', 'message'].forEach(key => {
const value = notification[key];
if (typeof value === 'object') {
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
} }
}));
return { notifications }; return message;
}; };
const mapDispatchToProps = (dispatch) => { const mapStateToProps = (state, { intl }) => ({
return { notifications: getAlerts(state).map(alert => ({
onDismiss: alert => { ...alert,
action: formatIfNeeded(intl, alert.action, alert.values),
title: formatIfNeeded(intl, alert.title, alert.values),
message: formatIfNeeded(intl, alert.message, alert.values),
})),
});
const mapDispatchToProps = (dispatch) => ({
onDismiss (alert) {
dispatch(dismissAlert(alert)); dispatch(dismissAlert(alert));
}, },
}; });
};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack)); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));

View file

@ -22,7 +22,7 @@ import { clearHeight } from '../../actions/height_cache';
import { expandNotifications } from '../../actions/notifications'; import { expandNotifications } from '../../actions/notifications';
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
import { expandHomeTimeline } from '../../actions/timelines'; import { expandHomeTimeline } from '../../actions/timelines';
import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state'; import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding } from '../../initial_state';
import BundleColumnError from './components/bundle_column_error'; import BundleColumnError from './components/bundle_column_error';
import Header from './components/header'; import Header from './components/header';
@ -170,7 +170,7 @@ class SwitchingColumnsArea extends PureComponent {
} }
} else if (singleUserMode && owner && initialState?.accounts[owner]) { } else if (singleUserMode && owner && initialState?.accounts[owner]) {
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />; redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
} else if (showTrends && trendsAsLanding) { } else if (trendsEnabled && trendsAsLanding) {
redirect = <Redirect from='/' to='/explore' exact />; redirect = <Redirect from='/' to='/explore' exact />;
} else { } else {
redirect = <Redirect from='/' to='/about' exact />; redirect = <Redirect from='/' to='/about' exact />;

View file

@ -69,12 +69,13 @@
* @property {boolean} reduce_motion * @property {boolean} reduce_motion
* @property {string} repository * @property {string} repository
* @property {boolean} search_enabled * @property {boolean} search_enabled
* @property {boolean} trends_enabled
* @property {boolean} single_user_mode * @property {boolean} single_user_mode
* @property {string} source_url * @property {string} source_url
* @property {string} streaming_api_base_url * @property {string} streaming_api_base_url
* @property {boolean} timeline_preview * @property {boolean} timeline_preview
* @property {string} title * @property {string} title
* @property {boolean} trends * @property {boolean} show_trends
* @property {boolean} trends_as_landing_page * @property {boolean} trends_as_landing_page
* @property {boolean} unfollow_modal * @property {boolean} unfollow_modal
* @property {boolean} use_blurhash * @property {boolean} use_blurhash
@ -122,7 +123,8 @@ export const reduceMotion = getMeta('reduce_motion');
export const registrationsOpen = getMeta('registrations_open'); export const registrationsOpen = getMeta('registrations_open');
export const repository = getMeta('repository'); export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled'); export const searchEnabled = getMeta('search_enabled');
export const showTrends = getMeta('trends'); export const trendsEnabled = getMeta('trends_enabled');
export const showTrends = getMeta('show_trends');
export const singleUserMode = getMeta('single_user_mode'); export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url'); export const source_url = getMeta('source_url');
export const timelinePreview = getMeta('timeline_preview'); export const timelinePreview = getMeta('timeline_preview');

View file

@ -135,6 +135,8 @@
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Remote only",
"compose.language.change": "Change language", "compose.language.change": "Change language",
"compose.language.search": "Search languages...", "compose.language.search": "Search languages...",
"compose.published.body": "Post published.",
"compose.published.open": "Open",
"compose_form.direct_message_warning_learn_more": "Learn more", "compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.", "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.", "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
@ -616,6 +618,8 @@
"status.history.created": "{name} created {date}", "status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}", "status.history.edited": "{name} edited {date}",
"status.load_more": "Load more", "status.load_more": "Load more",
"status.media.open": "Click to open",
"status.media.show": "Click to show",
"status.media_hidden": "Media hidden", "status.media_hidden": "Media hidden",
"status.mention": "Mention @{name}", "status.mention": "Mention @{name}",
"status.more": "More", "status.more": "More",
@ -646,7 +650,7 @@
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}", "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {{attachmentCount} attachments}}",
"status.translate": "Translate", "status.translate": "Translate",
"status.translated_from_with": "Translated from {lang} using {provider}", "status.translated_from_with": "Translated from {lang} using {provider}",
"status.uncached_media_warning": "Not available", "status.uncached_media_warning": "Preview not available",
"status.unmute_conversation": "Unmute conversation", "status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile", "status.unpin": "Unpin from profile",
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.", "subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",

View file

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { import {
ALERT_SHOW, ALERT_SHOW,
@ -8,17 +8,20 @@ import {
const initialState = ImmutableList([]); const initialState = ImmutableList([]);
let id = 0;
const addAlert = (state, alert) =>
state.push({
key: id++,
...alert,
});
export default function alerts(state = initialState, action) { export default function alerts(state = initialState, action) {
switch(action.type) { switch(action.type) {
case ALERT_SHOW: case ALERT_SHOW:
return state.push(ImmutableMap({ return addAlert(state, action.alert);
key: state.size > 0 ? state.last().get('key') + 1 : 0,
title: action.title,
message: action.message,
message_values: action.message_values,
}));
case ALERT_DISMISS: case ALERT_DISMISS:
return state.filterNot(item => item.get('key') === action.alert.key); return state.filterNot(item => item.key === action.alert.key);
case ALERT_CLEAR: case ALERT_CLEAR:
return state.clear(); return state.clear();
default: default:

View file

@ -26,7 +26,6 @@ import lists from './lists';
import markers from './markers'; import markers from './markers';
import media_attachments from './media_attachments'; import media_attachments from './media_attachments';
import meta from './meta'; import meta from './meta';
import { missedUpdatesReducer } from './missed_updates';
import { modalReducer } from './modal'; import { modalReducer } from './modal';
import mutes from './mutes'; import mutes from './mutes';
import notifications from './notifications'; import notifications from './notifications';
@ -82,7 +81,6 @@ const reducers = {
suggestions, suggestions,
polls, polls,
trends, trends,
missed_updates: missedUpdatesReducer,
markers, markers,
picture_in_picture, picture_in_picture,
history, history,

View file

@ -1,33 +0,0 @@
import { Record } from 'immutable';
import type { Action } from 'redux';
import { focusApp, unfocusApp } from '../actions/app';
import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
interface MissedUpdatesState {
focused: boolean;
unread: number;
}
const initialState = Record<MissedUpdatesState>({
focused: true,
unread: 0,
})();
export function missedUpdatesReducer(
state = initialState,
action: Action<string>
) {
switch (action.type) {
case focusApp.type:
return state.set('focused', true).set('unread', 0);
case unfocusApp.type:
return state.set('focused', false);
case NOTIFICATIONS_UPDATE:
return state.get('focused')
? state
: state.update('unread', (x) => x + 1);
default:
return state;
}
}

View file

@ -84,26 +84,16 @@ export const makeGetPictureInPicture = () => {
})); }));
}; };
const getAlertsBase = state => state.get('alerts'); const ALERT_DEFAULTS = {
export const getAlerts = createSelector([getAlertsBase], (base) => {
let arr = [];
base.forEach(item => {
arr.push({
message: item.get('message'),
message_values: item.get('message_values'),
title: item.get('title'),
key: item.get('key'),
dismissAfter: 5000, dismissAfter: 5000,
barStyle: { style: false,
zIndex: 200, };
},
});
});
return arr; export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
}); alerts.map(item => ({
...ALERT_DEFAULTS,
...item,
})).toArray());
export const makeGetNotification = () => createSelector([ export const makeGetNotification = () => createSelector([
(_, base) => base, (_, base) => base,

View file

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

View file

@ -15,7 +15,8 @@
.status__content a, .status__content a,
.link-footer a, .link-footer a,
.reply-indicator__content a, .reply-indicator__content a,
.status__content__read-more-button { .status__content__read-more-button,
.status__content__translate-button {
text-decoration: underline; text-decoration: underline;
&:hover, &:hover,

View file

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

View file

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

View file

@ -981,7 +981,8 @@ body > [data-popper-placement] {
max-height: 22px * 15; // 15 lines is roughly above 500 characters max-height: 22px * 15; // 15 lines is roughly above 500 characters
} }
.status__content__read-more-button { .status__content__read-more-button,
.status__content__translate-button {
display: block; display: block;
font-size: 15px; font-size: 15px;
line-height: 22px; line-height: 22px;
@ -1669,10 +1670,6 @@ a.account__display-name {
color: inherit; color: inherit;
} }
.detailed-status .button.logo-button {
margin-bottom: 15px;
}
.detailed-status__display-name { .detailed-status__display-name {
color: $darker-text-color; color: $darker-text-color;
display: flex; display: flex;
@ -4212,34 +4209,31 @@ a.status-card.compact:hover {
} }
&__overlay { &__overlay {
display: block; display: flex;
background: transparent; align-items: center;
justify-content: center;
background: rgba($black, 0.5);
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 0;
margin: 0;
border: 0; border: 0;
border-radius: 4px;
&__label { &__label {
display: inline-block; display: flex;
background: rgba($base-overlay-background, 0.5); align-items: center;
border-radius: 8px; justify-content: center;
padding: 8px 12px; gap: 8px;
flex-direction: column;
color: $primary-text-color; color: $primary-text-color;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
} }
&:hover, &__action {
&:focus, font-weight: 400;
&:active { font-size: 13px;
.spoiler-button__overlay__label {
background: rgba($base-overlay-background, 0.8);
}
}
&:disabled {
.spoiler-button__overlay__label {
background: rgba($base-overlay-background, 0.5);
}
} }
} }
} }
@ -5786,6 +5780,7 @@ a.status-card.compact:hover {
&__toggle { &__toggle {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 10px;
& > span { & > span {
font-size: 17px; font-size: 17px;
@ -9076,3 +9071,62 @@ noscript {
} }
} }
} }
.notification-list {
position: fixed;
bottom: 2rem;
inset-inline-start: 0;
z-index: 999;
display: flex;
flex-direction: column;
gap: 4px;
}
.notification-bar {
flex: 0 0 auto;
position: relative;
inset-inline-start: -100%;
width: auto;
padding: 15px;
margin: 0;
color: $white;
background: rgba($black, 0.85);
backdrop-filter: blur(8px);
border: 1px solid rgba(lighten($classic-base-color, 4%), 0.85);
border-radius: 8px;
box-shadow: 0 10px 15px -3px rgba($base-shadow-color, 0.25),
0 4px 6px -4px rgba($base-shadow-color, 0.25);
cursor: default;
transition: 0.5s cubic-bezier(0.89, 0.01, 0.5, 1.1);
transform: translateZ(0);
font-size: 15px;
line-height: 21px;
&.notification-bar-active {
inset-inline-start: 1rem;
}
}
.notification-bar-title {
margin-inline-end: 5px;
}
.notification-bar-title,
.notification-bar-action {
font-weight: 700;
}
.notification-bar-action {
text-transform: uppercase;
margin-inline-start: 10px;
cursor: pointer;
color: $blurple-300;
border-radius: 4px;
padding: 0 4px;
&:hover,
&:focus,
&:active {
background: rgba($ui-base-color, 0.85);
}
}

View file

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

View file

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

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