From 350a56119f7b126fdb8003dd431d05bf0e1e46f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Vigui=C3=A9?= Date: Thu, 6 Jul 2023 12:23:07 +0200 Subject: [PATCH] merge upstream --- .bundler-audit.yml | 3 + .circleci/config.yml | 225 - .codeclimate.yml | 39 - .deepsource.toml | 23 - .devcontainer/Dockerfile | 12 +- .devcontainer/devcontainer.json | 33 +- .devcontainer/docker-compose.yml | 23 +- .devcontainer/post-create.sh | 13 +- .devcontainer/welcome-message.txt | 8 + .editorconfig | 1 + .eslintrc.js | 337 +- .github/renovate.json5 | 114 + .github/workflows/build-image.yml | 26 +- .github/workflows/build-nightly.yml | 60 + .github/workflows/check-i18n.yml | 29 +- .../workflows/haml-lint-problem-matcher.json | 17 + .github/workflows/lint-css.yml | 52 + .github/workflows/lint-haml.yml | 47 + .github/workflows/lint-js.yml | 55 + .github/workflows/lint-json.yml | 8 +- .github/workflows/lint-md.yml | 44 + .github/workflows/lint-ruby.yml | 50 + .github/workflows/lint-yml.yml | 8 +- .github/workflows/linter.yml | 83 - .github/workflows/rebase-needed.yml | 19 + .github/workflows/test-js.yml | 48 + .../workflows/test-migrations-one-step.yml | 111 + .../workflows/test-migrations-two-step.yml | 119 + .github/workflows/test-ruby.yml | 151 + .haml-lint.yml | 106 +- .haml-lint_todo.yml | 106 + .husky/pre-commit | 4 + .nvmrc | 2 +- .prettierignore | 36 +- .prettierrc.js | 3 +- .profile | 2 +- .rubocop.yml | 538 +- .rubocop_todo.yml | 1452 +++ .ruby-version | 2 +- .yarnclean | 3 + Aptfile | 1 + CHANGELOG.md | 336 +- CODE_OF_CONDUCT.md | 20 +- CONTRIBUTING.md | 28 +- Capfile | 1 + Dockerfile | 21 +- Gemfile | 149 +- Gemfile.lock | 566 +- README.md | 4 +- SECURITY.md | 5 +- app/chewy/accounts_index.rb | 52 +- app/controllers/about_controller.rb | 2 +- app/controllers/accounts_controller.rb | 5 +- .../activitypub/base_controller.rb | 4 - .../activitypub/collections_controller.rb | 3 +- .../followers_synchronizations_controller.rb | 3 +- .../activitypub/outboxes_controller.rb | 8 +- .../activitypub/replies_controller.rb | 3 +- .../admin/announcements_controller.rb | 8 +- app/controllers/admin/base_controller.rb | 6 + app/controllers/admin/dashboard_controller.rb | 12 - .../admin/domain_blocks_controller.rb | 50 +- .../admin/email_domain_blocks_controller.rb | 6 - app/controllers/admin/roles_controller.rb | 8 +- app/controllers/admin/rules_controller.rb | 8 +- .../admin/warning_presets_controller.rb | 8 +- app/controllers/admin/webhooks_controller.rb | 23 +- app/controllers/api/base_controller.rb | 9 +- .../api/v1/accounts/credentials_controller.rb | 12 +- .../accounts/follower_accounts_controller.rb | 9 +- .../accounts/following_accounts_controller.rb | 9 +- .../api/v1/accounts/lookup_controller.rb | 1 + .../api/v1/accounts/statuses_controller.rb | 9 +- app/controllers/api/v1/accounts_controller.rb | 5 +- .../api/v1/admin/accounts_controller.rb | 4 +- .../canonical_email_blocks_controller.rb | 2 +- .../api/v1/admin/domain_allows_controller.rb | 26 +- .../api/v1/admin/domain_blocks_controller.rb | 20 +- .../admin/email_domain_blocks_controller.rb | 18 +- .../api/v1/admin/ip_blocks_controller.rb | 14 +- .../preview_card_providers_controller.rb | 72 + .../api/v1/admin/trends/links_controller.rb | 31 +- .../v1/admin/trends/statuses_controller.rb | 31 +- .../api/v1/admin/trends/tags_controller.rb | 23 +- .../api/v1/announcements_controller.rb | 4 +- app/controllers/api/v1/blocks_controller.rb | 8 +- .../api/v1/conversations_controller.rb | 28 +- .../api/v1/custom_emojis_controller.rb | 4 +- .../api/v1/directories_controller.rb | 35 +- .../api/v1/domain_blocks_controller.rb | 8 +- .../api/v1/emails/confirmations_controller.rb | 16 +- .../api/v1/endorsements_controller.rb | 8 +- .../api/v1/favourites_controller.rb | 8 +- .../api/v1/featured_tags_controller.rb | 6 +- app/controllers/api/v1/filters_controller.rb | 8 +- .../api/v1/follow_requests_controller.rb | 8 +- .../api/v1/instances/activity_controller.rb | 5 +- .../v1/instances/domain_blocks_controller.rb | 9 +- .../extended_descriptions_controller.rb | 10 +- .../api/v1/instances/peers_controller.rb | 11 +- .../instances/privacy_policies_controller.rb | 4 +- .../api/v1/instances/rules_controller.rb | 9 + .../translation_languages_controller.rb | 25 + .../api/v1/instances_controller.rb | 11 +- .../api/v1/lists/accounts_controller.rb | 8 +- app/controllers/api/v1/lists_controller.rb | 2 +- app/controllers/api/v1/media_controller.rb | 11 +- app/controllers/api/v1/mutes_controller.rb | 8 +- .../api/v1/notifications_controller.rb | 12 +- app/controllers/api/v1/polls_controller.rb | 1 + .../api/v1/push/subscriptions_controller.rb | 8 +- .../api/v1/scheduled_statuses_controller.rb | 8 +- .../favourited_by_accounts_controller.rb | 9 +- .../api/v1/statuses/histories_controller.rb | 7 +- .../reblogged_by_accounts_controller.rb | 9 +- .../api/v1/statuses/reblogs_controller.rb | 6 +- app/controllers/api/v1/statuses_controller.rb | 11 + .../api/v1/streaming_controller.rb | 6 +- app/controllers/api/v1/tags_controller.rb | 2 + .../api/v1/timelines/public_controller.rb | 3 +- .../api/v1/timelines/tag_controller.rb | 1 + .../api/v1/trends/links_controller.rb | 15 +- .../api/v1/trends/statuses_controller.rb | 15 +- .../api/v1/trends/tags_controller.rb | 13 +- .../api/v2/admin/accounts_controller.rb | 12 +- .../api/v2/filters/keywords_controller.rb | 8 +- .../api/v2/filters/statuses_controller.rb | 8 +- app/controllers/api/v2/filters_controller.rb | 8 +- .../api/v2/instances_controller.rb | 2 +- app/controllers/api/v2/media_controller.rb | 3 +- app/controllers/api/v2/search_controller.rb | 4 +- app/controllers/application_controller.rb | 45 +- .../auth/confirmations_controller.rb | 20 +- .../auth/omniauth_callbacks_controller.rb | 2 +- .../auth/registrations_controller.rb | 18 +- app/controllers/auth/sessions_controller.rb | 8 +- app/controllers/auth/setup_controller.rb | 21 +- .../authorize_interactions_controller.rb | 2 +- app/controllers/backups_controller.rb | 31 + .../concerns/account_controller_concern.rb | 3 +- .../concerns/api_caching_concern.rb | 13 + app/controllers/concerns/cache_concern.rb | 191 +- app/controllers/concerns/captcha_concern.rb | 5 + .../concerns/rate_limit_headers.rb | 14 +- .../concerns/session_tracking_concern.rb | 1 + .../concerns/signature_verification.rb | 14 +- app/controllers/concerns/theming_concern.rb | 2 +- .../two_factor_authentication_concern.rb | 16 +- .../concerns/web_app_controller_concern.rb | 6 + app/controllers/custom_css_controller.rb | 12 +- app/controllers/disputes/base_controller.rb | 5 + app/controllers/emojis_controller.rb | 11 +- .../filters/statuses_controller.rb | 9 +- app/controllers/filters_controller.rb | 9 +- .../follower_accounts_controller.rb | 5 +- .../following_accounts_controller.rb | 5 +- app/controllers/home_controller.rb | 2 +- app/controllers/instance_actors_controller.rb | 11 +- app/controllers/intents_controller.rb | 2 +- app/controllers/invites_controller.rb | 5 + .../mail_subscriptions_controller.rb | 41 + app/controllers/manifests_controller.rb | 7 +- app/controllers/media_controller.rb | 5 +- app/controllers/media_proxy_controller.rb | 5 +- .../oauth/authorizations_controller.rb | 2 +- .../authorized_applications_controller.rb | 17 + app/controllers/privacy_controller.rb | 2 +- app/controllers/relationships_controller.rb | 11 +- .../settings/applications_controller.rb | 12 +- app/controllers/settings/base_controller.rb | 2 +- .../settings/exports_controller.rb | 2 +- .../settings/flavours_controller.rb | 18 +- .../settings/imports_controller.rb | 96 +- .../preferences/appearance_controller.rb | 2 +- .../settings/preferences/base_controller.rb | 24 + .../preferences/notifications_controller.rb | 2 +- .../settings/preferences/other_controller.rb | 2 +- .../settings/preferences_controller.rb | 65 - .../otp_authentication_controller.rb | 9 - .../webauthn_credentials_controller.rb | 7 +- .../settings/verifications_controller.rb | 15 + .../statuses_cleanup_controller.rb | 5 + app/controllers/statuses_controller.rb | 12 +- app/controllers/tags_controller.rb | 17 +- .../well_known/host_meta_controller.rb | 4 +- .../well_known/nodeinfo_controller.rb | 6 +- .../well_known/webfinger_controller.rb | 22 +- app/helpers/accounts_helper.rb | 2 +- app/helpers/admin/action_logs_helper.rb | 2 +- app/helpers/admin/announcements_helper.rb | 11 - app/helpers/admin/dashboard_helper.rb | 24 +- app/helpers/admin/trends/statuses_helper.rb | 12 +- app/helpers/application_helper.rb | 51 +- app/helpers/branding_helper.rb | 18 +- app/helpers/domain_control_helper.rb | 12 +- app/helpers/email_helper.rb | 2 +- app/helpers/formatting_helper.rb | 40 +- app/helpers/home_helper.rb | 14 +- app/helpers/instance_helper.rb | 20 +- app/helpers/jsonld_helper.rb | 28 +- app/helpers/languages_helper.rb | 6 +- app/helpers/media_component_helper.rb | 111 + app/helpers/react_component_helper.rb | 23 + app/helpers/settings_helper.rb | 13 - app/helpers/statuses_helper.rb | 88 +- app/javascript/core/admin.js | 1 + app/javascript/core/embed.js | 2 +- app/javascript/core/mailer.js | 2 +- app/javascript/core/public.js | 4 +- app/javascript/core/settings.js | 6 +- app/javascript/core/theme.yml | 1 + .../core/two_factor_authentication.js | 4 +- .../flavours/glitch/actions/account_notes.js | 14 +- .../flavours/glitch/actions/accounts.js | 197 +- .../flavours/glitch/actions/alerts.js | 6 +- .../flavours/glitch/actions/announcements.js | 1 + app/javascript/flavours/glitch/actions/app.js | 6 - app/javascript/flavours/glitch/actions/app.ts | 9 + .../flavours/glitch/actions/blocks.js | 19 +- .../flavours/glitch/actions/bookmarks.js | 17 +- .../flavours/glitch/actions/boosts.js | 7 +- .../flavours/glitch/actions/columns.js | 6 +- .../flavours/glitch/actions/compose.js | 130 +- .../flavours/glitch/actions/conversations.js | 1 + .../flavours/glitch/actions/custom_emojis.js | 8 +- .../flavours/glitch/actions/directory.js | 3 +- .../flavours/glitch/actions/domain_blocks.js | 32 +- .../flavours/glitch/actions/emojis.js | 2 +- .../flavours/glitch/actions/favourites.js | 17 +- .../flavours/glitch/actions/filters.js | 10 +- .../flavours/glitch/actions/height_cache.js | 4 +- .../flavours/glitch/actions/history.js | 1 + .../glitch/actions/importer/normalizer.js | 41 +- .../flavours/glitch/actions/interactions.js | 81 +- .../flavours/glitch/actions/lists.js | 7 +- .../flavours/glitch/actions/local_settings.js | 20 +- .../flavours/glitch/actions/markers.js | 20 +- .../flavours/glitch/actions/modal.js | 18 - .../flavours/glitch/actions/modal.ts | 17 + .../flavours/glitch/actions/mutes.js | 22 +- .../flavours/glitch/actions/notifications.js | 61 +- .../flavours/glitch/actions/onboarding.js | 6 +- .../glitch/actions/picture_in_picture.js | 3 +- .../flavours/glitch/actions/pin_statuses.js | 14 +- .../flavours/glitch/actions/polls.js | 1 + .../actions/push_notifications/index.js | 2 +- .../actions/push_notifications/registerer.js | 3 +- .../flavours/glitch/actions/reports.js | 10 +- .../flavours/glitch/actions/search.js | 13 +- .../flavours/glitch/actions/server.js | 40 + .../flavours/glitch/actions/settings.js | 8 +- .../flavours/glitch/actions/statuses.js | 55 +- .../flavours/glitch/actions/store.js | 5 +- .../flavours/glitch/actions/streaming.js | 61 +- .../flavours/glitch/actions/suggestions.js | 11 +- .../flavours/glitch/actions/tags.js | 14 +- .../flavours/glitch/actions/timelines.js | 41 +- .../flavours/glitch/actions/trends.js | 1 + app/javascript/flavours/glitch/api.js | 6 +- .../flavours/glitch/base_polyfills.js | 47 - .../glitch/blurhash.ts} | 10 +- .../glitch/compare_id.ts} | 2 +- .../components/{account.js => account.jsx} | 45 +- .../admin/{Counter.js => Counter.jsx} | 26 +- .../admin/{Dimension.js => Dimension.jsx} | 14 +- .../glitch/components/admin/ImpactReport.jsx | 91 + ...onSelector.js => ReportReasonSelector.jsx} | 20 +- .../admin/{Retention.js => Retention.jsx} | 11 +- .../admin/{Trends.js => Trends.jsx} | 9 +- .../glitch/components/animated_number.js | 76 - .../glitch/components/animated_number.tsx | 82 + .../glitch/components/attachment_list.jsx} | 11 +- ...suggest_emoji.js => autosuggest_emoji.jsx} | 6 +- .../glitch/components/autosuggest_hashtag.js | 42 - .../glitch/components/autosuggest_hashtag.tsx | 42 + ...suggest_input.js => autosuggest_input.jsx} | 39 +- ...t_textarea.js => autosuggest_textarea.jsx} | 42 +- .../flavours/glitch/components/avatar.js | 79 - .../flavours/glitch/components/avatar.tsx | 55 + ...atar_composite.js => avatar_composite.jsx} | 16 +- .../{avatar_overlay.js => avatar_overlay.jsx} | 6 +- .../flavours/glitch/components/blurhash.js | 65 - .../flavours/glitch/components/blurhash.tsx | 49 + .../components/{button.js => button.jsx} | 9 +- .../glitch/components/{check.js => check.jsx} | 2 - .../glitch/components/circular_progress.tsx | 27 + .../components/{column.js => column.jsx} | 20 +- .../glitch/components/column_back_button.jsx} | 23 +- .../components/column_back_button_slim.js | 37 - .../components/column_back_button_slim.jsx | 37 + .../{column_header.js => column_header.jsx} | 57 +- .../{common_counter.js => common_counter.jsx} | 3 - ...sable_banner.js => dismissable_banner.jsx} | 14 +- .../glitch/components/display_name.js | 102 - .../glitch/components/display_name.tsx | 124 + .../flavours/glitch/components/domain.js | 42 - .../flavours/glitch/components/domain.tsx | 45 + .../glitch/components/dropdown_menu.jsx} | 79 +- .../containers/dropdown_menu_container.js | 1 + .../edited_timestamp/{index.js => index.jsx} | 29 +- .../{error_boundary.js => error_boundary.jsx} | 18 +- .../flavours/glitch/components/gifv.js | 73 - .../flavours/glitch/components/gifv.tsx | 71 + .../components/{hashtag.js => hashtag.jsx} | 30 +- .../flavours/glitch/components/icon.js | 21 - .../flavours/glitch/components/icon.tsx | 22 + .../{icon_button.js => icon_button.tsx} | 113 +- .../glitch/components/icon_with_badge.js | 22 - .../glitch/components/icon_with_badge.tsx | 26 + .../flavours/glitch/components/image.js | 33 - .../{inline_account.js => inline_account.jsx} | 11 +- ...e.js => intersection_observer_article.jsx} | 27 +- .../glitch/components/{link.js => link.jsx} | 8 +- .../flavours/glitch/components/load_gap.js | 34 - .../flavours/glitch/components/load_gap.tsx | 34 + .../flavours/glitch/components/load_more.js | 27 - .../flavours/glitch/components/load_more.tsx | 24 + .../glitch/components/load_pending.js | 22 - .../glitch/components/load_pending.tsx | 18 + .../glitch/components/loading_indicator.js | 32 - .../glitch/components/loading_indicator.tsx | 7 + .../flavours/glitch/components/logo.js | 10 - .../flavours/glitch/components/logo.jsx | 14 + ...a_attachments.js => media_attachments.jsx} | 25 +- .../{media_gallery.js => media_gallery.jsx} | 136 +- .../glitch/components/missing_indicator.js | 29 - .../{modal_root.js => modal_root.jsx} | 22 +- ...gation_portal.js => navigation_portal.jsx} | 27 +- .../components/not_signed_in_indicator.js | 12 - .../components/not_signed_in_indicator.tsx | 14 + ...tons.js => notification_purge_buttons.jsx} | 19 +- .../{permalink.js => permalink.jsx} | 10 +- .../picture_in_picture_placeholder.js | 69 - .../picture_in_picture_placeholder.jsx | 33 + .../glitch/components/{poll.js => poll.jsx} | 67 +- .../glitch/components/radio_button.js | 35 - .../glitch/components/radio_button.tsx | 35 + ...ndicator.js => regeneration_indicator.jsx} | 2 +- .../glitch/components/relative_timestamp.js | 199 - .../glitch/components/relative_timestamp.tsx | 282 + ...scrollable_list.js => scrollable_list.jsx} | 117 +- .../{server_banner.js => server_banner.jsx} | 20 +- .../glitch/components/server_hero_image.tsx | 36 + .../{setting_text.js => setting_text.jsx} | 7 +- .../{short_number.js => short_number.jsx} | 13 +- .../flavours/glitch/components/skeleton.js | 11 - .../flavours/glitch/components/skeleton.tsx | 12 + .../flavours/glitch/components/spoilers.js | 50 - .../components/{status.js => status.jsx} | 208 +- ...us_action_bar.js => status_action_bar.jsx} | 96 +- .../{status_content.js => status_content.jsx} | 84 +- .../{status_header.js => status_header.jsx} | 14 +- .../{status_icons.js => status_icons.jsx} | 42 +- .../{status_list.js => status_list.jsx} | 31 +- .../{status_prepend.js => status_prepend.jsx} | 17 +- ...ity_icon.js => status_visibility_icon.jsx} | 11 +- .../glitch/components/timeline_hint.js | 18 - .../glitch/components/timeline_hint.tsx | 27 + ...unt_container.js => account_container.jsx} | 20 +- .../glitch/containers/admin_component.js | 26 - .../glitch/containers/admin_component.jsx | 22 + .../glitch/containers/compose_container.js | 41 - .../glitch/containers/compose_container.jsx | 30 + .../glitch/containers/domain_container.js | 33 - .../glitch/containers/domain_container.jsx} | 20 +- .../containers/dropdown_menu_container.js | 20 +- ...intersection_observer_article_container.js | 3 +- .../containers/{mastodon.js => mastodon.jsx} | 31 +- ...media_container.js => media_container.jsx} | 62 +- .../notification_purge_buttons_container.js | 18 +- .../glitch/containers/poll_container.js | 3 +- .../glitch/containers/status_container.js | 123 +- .../features/about/{index.js => index.jsx} | 46 +- .../{account_note.js => account_note.jsx} | 20 +- .../{action_bar.js => action_bar.jsx} | 30 +- .../{featured_tags.js => featured_tags.jsx} | 9 +- .../components/follow_request_note.jsx} | 7 +- .../components/{header.js => header.jsx} | 67 +- ...mn_header.js => profile_column_header.jsx} | 11 +- .../containers/account_note_container.js | 2 + .../containers/featured_tags_container.js | 8 +- .../follow_request_note_container.js | 4 +- .../account/{navigation.js => navigation.jsx} | 9 +- .../{media_item.js => media_item.jsx} | 25 +- .../account_gallery/{index.js => index.jsx} | 73 +- .../components/{header.js => header.jsx} | 50 +- ...count_hint.js => limited_account_hint.jsx} | 16 +- .../components/memorial_note.jsx | 11 + .../{moved_note.js => moved_note.jsx} | 19 +- ...ader_container.js => header_container.jsx} | 95 +- .../account_timeline/{index.js => index.jsx} | 60 +- .../features/audio/{index.js => index.jsx} | 142 +- .../features/blocks/{index.js => index.jsx} | 30 +- .../{index.js => index.jsx} | 29 +- .../{index.js => index.jsx} | 12 +- ...column_settings.js => column_settings.jsx} | 12 +- .../containers/column_settings_container.js | 6 +- .../{index.js => index.jsx} | 46 +- .../{action_bar.js => action_bar.jsx} | 21 +- ...est_account.js => autosuggest_account.jsx} | 6 +- ...acter_counter.js => character_counter.jsx} | 5 +- .../{compose_form.js => compose_form.jsx} | 96 +- .../components/{dropdown.js => dropdown.jsx} | 42 +- .../{dropdown_menu.js => dropdown_menu.jsx} | 53 +- ..._dropdown.js => emoji_picker_dropdown.jsx} | 80 +- .../components/{header.js => header.jsx} | 32 +- ...uage_dropdown.js => language_dropdown.jsx} | 63 +- .../{navigation_bar.js => navigation_bar.jsx} | 13 +- .../components/{options.js => options.jsx} | 49 +- .../{poll_form.js => poll_form.jsx} | 40 +- ...ivacy_dropdown.js => privacy_dropdown.jsx} | 12 +- .../{publisher.js => publisher.jsx} | 37 +- ...reply_indicator.js => reply_indicator.jsx} | 15 +- .../components/{search.js => search.jsx} | 37 +- .../{search_results.js => search_results.jsx} | 24 +- ...xt_icon_button.js => text_icon_button.jsx} | 4 +- .../{textarea_icons.js => textarea_icons.jsx} | 14 +- .../components/{upload.js => upload.jsx} | 30 +- .../{upload_form.js => upload_form.jsx} | 7 +- ...upload_progress.js => upload_progress.jsx} | 14 +- .../components/{warning.js => warning.jsx} | 8 +- .../autosuggest_account_container.js | 4 +- .../containers/compose_form_container.js | 71 +- .../compose/containers/dropdown_container.js | 8 +- .../emoji_picker_dropdown_container.js | 11 +- .../compose/containers/header_container.js | 24 +- .../containers/language_dropdown_container.js | 9 +- .../containers/navigation_container.js | 22 +- .../compose/containers/options_container.js | 13 +- .../compose/containers/poll_form_container.js | 10 +- .../containers/privacy_dropdown_container.js | 14 +- .../containers/reply_indicator_container.js | 2 + .../compose/containers/search_container.js | 2 + .../containers/search_results_container.js | 6 +- ...iner.js => sensitive_button_container.jsx} | 14 +- .../compose/containers/upload_container.js | 5 +- .../containers/upload_form_container.js | 1 + .../containers/upload_progress_container.js | 1 + ...ing_container.js => warning_container.jsx} | 19 +- .../features/compose/{index.js => index.jsx} | 39 +- .../glitch/features/compose/util/counter.js | 4 +- .../glitch/features/compose/util/url_regex.js | 2 +- ...column_settings.js => column_settings.jsx} | 13 +- .../{conversation.js => conversation.jsx} | 57 +- ...sations_list.js => conversations_list.jsx} | 23 +- .../containers/column_settings_container.js | 4 +- .../containers/conversation_container.js | 22 +- .../conversations_list_container.js | 4 +- .../direct_timeline/{index.js => index.jsx} | 37 +- .../{account_card.js => account_card.jsx} | 79 +- .../directory/{index.js => index.jsx} | 47 +- .../domain_blocks/{index.js => index.jsx} | 37 +- .../flavours/glitch/features/emoji/emoji.js | 112 +- .../features/emoji/emoji_compressed.d.ts | 51 + .../glitch/features/emoji/emoji_compressed.js | 21 +- .../features/emoji/emoji_mart_data_light.js | 41 - .../features/emoji/emoji_mart_data_light.ts | 52 + .../features/emoji/emoji_mart_search_light.js | 2 +- .../glitch/features/emoji/emoji_picker.js | 2 +- .../emoji/emoji_unicode_mapping_light.js | 14 +- .../glitch/features/emoji/emoji_utils.js | 14 +- .../features/emoji/unicode_to_filename.js | 3 + .../features/emoji/unicode_to_unified_name.js | 3 + .../components/{story.js => story.jsx} | 19 +- .../features/explore/{index.js => index.jsx} | 54 +- .../features/explore/{links.js => links.jsx} | 23 +- .../explore/{results.js => results.jsx} | 32 +- .../explore/{statuses.js => statuses.jsx} | 24 +- .../{suggestions.js => suggestions.jsx} | 22 +- .../features/explore/{tags.js => tags.jsx} | 24 +- .../{index.js => index.jsx} | 29 +- .../favourites/{index.js => index.jsx} | 32 +- ...added_to_filter.js => added_to_filter.jsx} | 28 +- .../{select_filter.js => select_filter.jsx} | 40 +- .../glitch/features/firehose/index.jsx | 231 + .../components/{account.js => account.jsx} | 24 +- .../{index.js => index.jsx} | 37 +- ...unt_authorize.js => account_authorize.jsx} | 17 +- .../containers/account_authorize_container.js | 6 +- .../follow_requests/{index.js => index.jsx} | 50 +- .../followed_tags/{index.js => index.jsx} | 24 +- .../followers/{index.js => index.jsx} | 37 +- .../following/{index.js => index.jsx} | 37 +- .../features/generic_not_found/index.js | 11 - .../{announcements.js => announcements.jsx} | 63 +- .../components/{trends.js => trends.jsx} | 11 +- .../containers/announcements_container.js | 10 +- .../containers/trends_container.js | 2 + .../getting_started/{index.js => index.jsx} | 62 +- .../{index.js => index.jsx} | 64 +- .../components/column_settings.jsx} | 17 +- .../containers/column_settings_container.js | 4 +- .../hashtag_timeline/{index.js => index.jsx} | 61 +- ...column_settings.js => column_settings.jsx} | 16 +- .../components/explore_prompt.jsx | 25 + .../containers/column_settings_container.js | 4 +- .../home_timeline/{index.js => index.jsx} | 94 +- .../interaction_modal/{index.js => index.jsx} | 42 +- .../{index.js => index.jsx} | 20 +- .../list_adder/components/account.jsx} | 20 +- .../components/{list.js => list.jsx} | 21 +- .../glitch/features/list_adder/index.jsx} | 21 +- .../components/{account.js => account.jsx} | 15 +- .../{edit_list_form.js => edit_list_form.jsx} | 23 +- .../components/{search.js => search.jsx} | 17 +- .../containers/account_container.js | 8 +- .../containers/search_container.js | 5 +- .../list_editor/{index.js => index.jsx} | 26 +- .../list_timeline/{index.js => index.jsx} | 103 +- .../{new_list_form.js => new_list_form.jsx} | 23 +- .../features/lists/{index.js => index.jsx} | 16 +- .../local_settings/{index.js => index.jsx} | 17 +- .../navigation/{index.js => index.jsx} | 13 +- .../navigation/item/{index.js => index.jsx} | 11 +- .../deprecated_item/{index.js => index.jsx} | 6 +- .../page/{index.js => index.jsx} | 53 +- .../page/item/{index.js => index.jsx} | 21 +- .../features/mutes/{index.js => index.jsx} | 37 +- .../{admin_report.js => admin_report.jsx} | 43 +- .../{admin_signup.js => admin_signup.jsx} | 27 +- ...lumn_button.js => clear_column_button.jsx} | 12 +- ...column_settings.js => column_settings.jsx} | 20 +- .../notifications/components/filter_bar.jsx} | 11 +- .../components/{follow.js => follow.jsx} | 27 +- .../{follow_request.js => follow_request.jsx} | 44 +- ..._button.js => grant_permission_button.jsx} | 7 +- .../{notification.js => notification.jsx} | 15 +- ...js => notifications_permission_banner.jsx} | 29 +- .../components/{overlay.js => overlay.jsx} | 15 +- ...pill_bar_button.js => pill_bar_button.jsx} | 14 +- .../components/{report.js => report.jsx} | 25 +- .../{setting_toggle.js => setting_toggle.jsx} | 10 +- .../containers/admin_report_container.js | 2 + .../containers/column_settings_container.js | 27 +- .../containers/filter_bar_container.js | 3 +- .../containers/follow_request_container.js | 5 +- .../containers/notification_container.js | 5 +- .../containers/overlay_container.js | 3 +- .../notifications/{index.js => index.jsx} | 127 +- .../components/{footer.js => footer.jsx} | 73 +- .../components/{header.js => header.jsx} | 25 +- .../{index.js => index.jsx} | 25 +- .../containers/account_container.js | 7 +- .../containers/search_container.js | 11 +- .../{index.js => index.jsx} | 22 +- .../pinned_statuses/{index.js => index.jsx} | 33 +- .../privacy_policy/{index.js => index.jsx} | 16 +- ...column_settings.js => column_settings.jsx} | 12 +- .../containers/column_settings_container.js | 8 +- .../public_timeline/{index.js => index.jsx} | 45 +- .../features/reblogs/{index.js => index.jsx} | 39 +- .../report/{category.js => category.jsx} | 20 +- .../report/{comment.js => comment.jsx} | 20 +- .../components/{option.js => option.jsx} | 12 +- ...atus_check_box.js => status_check_box.jsx} | 17 +- .../containers/status_check_box_container.js | 4 +- .../features/report/{rules.js => rules.jsx} | 17 +- .../report/{statuses.js => statuses.jsx} | 23 +- .../features/report/{thanks.js => thanks.jsx} | 22 +- .../compose/{index.js => index.jsx} | 9 +- .../{action_bar.js => action_bar.jsx} | 82 +- .../status/components/{card.js => card.jsx} | 95 +- ...detailed_status.js => detailed_status.jsx} | 114 +- .../containers/detailed_status_container.js | 68 +- .../features/status/{index.js => index.jsx} | 279 +- .../{index.js => index.jsx} | 24 +- .../{actions_modal.js => actions_modal.jsx} | 22 +- .../{audio_modal.js => audio_modal.jsx} | 23 +- .../{block_modal.js => block_modal.jsx} | 32 +- .../{boost_modal.js => boost_modal.jsx} | 45 +- .../ui/components/{bundle.js => bundle.jsx} | 18 +- ...olumn_error.js => bundle_column_error.jsx} | 28 +- ..._modal_error.js => bundle_modal_error.jsx} | 11 +- .../ui/components/{column.js => column.jsx} | 21 +- .../{column_header.js => column_header.jsx} | 12 +- .../{column_link.js => column_link.jsx} | 12 +- .../{column_loading.js => column_loading.jsx} | 4 +- ...mn_subheading.js => column_subheading.jsx} | 1 - .../{columns_area.js => columns_area.jsx} | 39 +- ...ory_modal.js => compare_history_modal.jsx} | 41 +- .../{compose_panel.js => compose_panel.jsx} | 31 +- ...mation_modal.js => confirmation_modal.jsx} | 19 +- ...modal.js => deprecated_settings_modal.jsx} | 22 +- ..._banner.js => disabled_account_banner.jsx} | 35 +- .../{doodle_modal.js => doodle_modal.jsx} | 59 +- .../ui/components/drawer_loading.jsx} | 2 - .../{embed_modal.js => embed_modal.jsx} | 19 +- ...favourite_modal.js => favourite_modal.jsx} | 36 +- .../{filter_modal.js => filter_modal.jsx} | 18 +- ...l_point_modal.js => focal_point_modal.jsx} | 89 +- ...ink.js => follow_requests_column_link.jsx} | 21 +- .../glitch/features/ui/components/header.js | 88 - .../glitch/features/ui/components/header.jsx | 117 + .../{image_loader.js => image_loader.jsx} | 20 +- .../features/ui/components/image_modal.jsx} | 15 +- .../{link_footer.js => link_footer.jsx} | 49 +- .../{list_panel.js => list_panel.jsx} | 13 +- .../{media_modal.js => media_modal.jsx} | 59 +- .../features/ui/components/modal_loading.jsx} | 4 +- .../{modal_root.js => modal_root.jsx} | 58 +- .../{mute_modal.js => mute_modal.jsx} | 33 +- ...vigation_panel.js => navigation_panel.jsx} | 42 +- .../components/notifications_counter_icon.js | 3 +- ...boarding_modal.js => onboarding_modal.jsx} | 58 +- .../{report_modal.js => report_modal.jsx} | 30 +- .../features/ui/components/sign_in_banner.js | 40 - .../features/ui/components/sign_in_banner.jsx | 44 + .../{upload_area.js => upload_area.jsx} | 15 +- .../{video_modal.js => video_modal.jsx} | 32 +- .../ui/components/zoomable_image.jsx} | 48 +- .../ui/containers/bundle_container.js | 3 +- .../ui/containers/columns_area_container.js | 9 +- .../ui/containers/loading_bar_container.js | 1 + .../features/ui/containers/modal_container.js | 22 +- .../ui/containers/notifications_container.js | 3 + .../ui/containers/status_list_container.js | 9 +- .../features/ui/{index.js => index.jsx} | 180 +- .../features/ui/util/async-components.js | 8 +- .../features/ui/util/optional_motion.js | 6 +- ...er_helpers.js => react_router_helpers.jsx} | 17 +- .../features/ui/util/reduced_motion.jsx} | 9 +- .../features/video/{index.js => index.jsx} | 170 +- .../flavours/glitch/hooks/useHovering.ts | 17 + .../flavours/glitch/initial_state.js | 23 +- app/javascript/flavours/glitch/is_mobile.js | 55 - app/javascript/flavours/glitch/is_mobile.ts | 43 + .../flavours/glitch/locales/af.json | 11 +- .../flavours/glitch/locales/an.json | 11 +- .../flavours/glitch/locales/ar.json | 4 + .../flavours/glitch/locales/ast.json | 4 + .../flavours/glitch/locales/be.json | 11 +- .../flavours/glitch/locales/bg.json | 4 + .../flavours/glitch/locales/bn.json | 4 + .../flavours/glitch/locales/br.json | 4 + .../flavours/glitch/locales/bs.json | 11 +- .../flavours/glitch/locales/ca.json | 4 + .../flavours/glitch/locales/ckb.json | 4 + .../flavours/glitch/locales/co.json | 4 + .../flavours/glitch/locales/cs.json | 5 +- .../flavours/glitch/locales/cy.json | 4 + .../flavours/glitch/locales/da.json | 4 + .../flavours/glitch/locales/de.json | 14 +- .../glitch/locales/defaultMessages.json | 1064 -- .../flavours/glitch/locales/el.json | 4 + .../flavours/glitch/locales/en-GB.json | 11 +- .../flavours/glitch/locales/en.json | 17 +- .../flavours/glitch/locales/eo.json | 45 +- .../flavours/glitch/locales/es-AR.json | 111 +- .../flavours/glitch/locales/es-MX.json | 111 +- .../flavours/glitch/locales/es.json | 183 +- .../flavours/glitch/locales/et.json | 4 + .../flavours/glitch/locales/eu.json | 4 + .../flavours/glitch/locales/fa.json | 4 + .../flavours/glitch/locales/fi.json | 4 + .../flavours/glitch/locales/fo.json | 11 +- .../flavours/glitch/locales/fr-QC.json | 5 +- .../flavours/glitch/locales/fr.json | 5 +- .../flavours/glitch/locales/fy.json | 11 +- .../flavours/glitch/locales/ga.json | 4 + .../flavours/glitch/locales/gd.json | 4 + .../flavours/glitch/locales/gl.json | 4 + .../flavours/glitch/locales/global_locale.ts | 22 + .../flavours/glitch/locales/he.json | 4 + .../flavours/glitch/locales/hi.json | 4 + .../flavours/glitch/locales/hr.json | 4 + .../flavours/glitch/locales/hu.json | 4 + .../flavours/glitch/locales/hy.json | 4 + .../flavours/glitch/locales/id.json | 4 + .../flavours/glitch/locales/ig.json | 11 +- .../flavours/glitch/locales/index.ts | 5 + .../flavours/glitch/locales/intl_provider.tsx | 57 + .../flavours/glitch/locales/io.json | 4 + .../flavours/glitch/locales/is.json | 4 + .../flavours/glitch/locales/it.json | 4 + .../flavours/glitch/locales/ja.json | 8 + .../flavours/glitch/locales/ka.json | 4 + .../flavours/glitch/locales/kab.json | 4 + .../flavours/glitch/locales/kk.json | 4 + .../flavours/glitch/locales/kn.json | 4 + .../flavours/glitch/locales/ko.json | 6 +- .../flavours/glitch/locales/ku.json | 4 + .../flavours/glitch/locales/kw.json | 4 + .../flavours/glitch/locales/la.json | 11 +- .../flavours/glitch/locales/load_locale.ts | 37 + .../flavours/glitch/locales/lt.json | 4 + .../flavours/glitch/locales/lv.json | 4 + .../flavours/glitch/locales/mk.json | 4 + .../flavours/glitch/locales/ml.json | 4 + .../flavours/glitch/locales/mr.json | 4 + .../flavours/glitch/locales/ms.json | 4 + .../flavours/glitch/locales/my.json | 11 +- .../flavours/glitch/locales/nl.json | 4 + .../flavours/glitch/locales/nn.json | 4 + .../flavours/glitch/locales/no.json | 4 + .../flavours/glitch/locales/oc.json | 4 + .../flavours/glitch/locales/pa.json | 4 + .../flavours/glitch/locales/pl.json | 154 +- .../flavours/glitch/locales/pt-BR.json | 12 +- .../flavours/glitch/locales/pt-PT.json | 23 + .../flavours/glitch/locales/ro.json | 4 + .../flavours/glitch/locales/ru.json | 6 + .../flavours/glitch/locales/sa.json | 4 + .../flavours/glitch/locales/sc.json | 4 + .../flavours/glitch/locales/sco.json | 11 +- .../flavours/glitch/locales/si.json | 4 + .../flavours/glitch/locales/sk.json | 4 + .../flavours/glitch/locales/sl.json | 4 + .../flavours/glitch/locales/sq.json | 4 + .../flavours/glitch/locales/sr-Latn.json | 4 + .../flavours/glitch/locales/sr.json | 4 + .../flavours/glitch/locales/sv.json | 4 + .../flavours/glitch/locales/szl.json | 13 +- .../flavours/glitch/locales/ta.json | 4 + .../flavours/glitch/locales/tai.json | 13 +- .../flavours/glitch/locales/te.json | 4 + .../flavours/glitch/locales/th.json | 4 + .../flavours/glitch/locales/tr.json | 4 + .../flavours/glitch/locales/tt.json | 4 + .../flavours/glitch/locales/ug.json | 4 + .../flavours/glitch/locales/uk.json | 4 + .../flavours/glitch/locales/ur.json | 4 + .../flavours/glitch/locales/vi.json | 4 + .../flavours/glitch/locales/whitelist_af.json | 2 - .../flavours/glitch/locales/whitelist_ar.json | 2 - .../glitch/locales/whitelist_ast.json | 2 - .../flavours/glitch/locales/whitelist_bg.json | 2 - .../flavours/glitch/locales/whitelist_bn.json | 2 - .../flavours/glitch/locales/whitelist_br.json | 2 - .../flavours/glitch/locales/whitelist_ca.json | 2 - .../glitch/locales/whitelist_ckb.json | 2 - .../flavours/glitch/locales/whitelist_co.json | 2 - .../flavours/glitch/locales/whitelist_cs.json | 2 - .../flavours/glitch/locales/whitelist_cy.json | 2 - .../flavours/glitch/locales/whitelist_da.json | 2 - .../flavours/glitch/locales/whitelist_de.json | 2 - .../flavours/glitch/locales/whitelist_el.json | 2 - .../flavours/glitch/locales/whitelist_en.json | 2 - .../flavours/glitch/locales/whitelist_eo.json | 2 - .../glitch/locales/whitelist_es-AR.json | 2 - .../glitch/locales/whitelist_es-MX.json | 2 - .../flavours/glitch/locales/whitelist_es.json | 2 - .../flavours/glitch/locales/whitelist_et.json | 2 - .../flavours/glitch/locales/whitelist_eu.json | 2 - .../flavours/glitch/locales/whitelist_fa.json | 2 - .../flavours/glitch/locales/whitelist_fi.json | 2 - .../flavours/glitch/locales/whitelist_fr.json | 2 - .../flavours/glitch/locales/whitelist_ga.json | 2 - .../flavours/glitch/locales/whitelist_gd.json | 2 - .../flavours/glitch/locales/whitelist_gl.json | 2 - .../flavours/glitch/locales/whitelist_he.json | 2 - .../flavours/glitch/locales/whitelist_hi.json | 2 - .../flavours/glitch/locales/whitelist_hr.json | 2 - .../flavours/glitch/locales/whitelist_hu.json | 2 - .../flavours/glitch/locales/whitelist_hy.json | 2 - .../flavours/glitch/locales/whitelist_id.json | 2 - .../flavours/glitch/locales/whitelist_io.json | 2 - .../flavours/glitch/locales/whitelist_is.json | 2 - .../flavours/glitch/locales/whitelist_it.json | 2 - .../flavours/glitch/locales/whitelist_ja.json | 2 - .../flavours/glitch/locales/whitelist_ka.json | 2 - .../glitch/locales/whitelist_kab.json | 2 - .../flavours/glitch/locales/whitelist_kk.json | 2 - .../flavours/glitch/locales/whitelist_kn.json | 2 - .../flavours/glitch/locales/whitelist_ko.json | 2 - .../flavours/glitch/locales/whitelist_ku.json | 2 - .../flavours/glitch/locales/whitelist_kw.json | 2 - .../flavours/glitch/locales/whitelist_lt.json | 2 - .../flavours/glitch/locales/whitelist_lv.json | 2 - .../flavours/glitch/locales/whitelist_mk.json | 2 - .../flavours/glitch/locales/whitelist_ml.json | 2 - .../flavours/glitch/locales/whitelist_mr.json | 2 - .../flavours/glitch/locales/whitelist_ms.json | 2 - .../flavours/glitch/locales/whitelist_nl.json | 2 - .../flavours/glitch/locales/whitelist_nn.json | 2 - .../flavours/glitch/locales/whitelist_no.json | 2 - .../flavours/glitch/locales/whitelist_oc.json | 2 - .../flavours/glitch/locales/whitelist_pa.json | 2 - .../flavours/glitch/locales/whitelist_pl.json | 2 - .../glitch/locales/whitelist_pt-BR.json | 2 - .../glitch/locales/whitelist_pt-PT.json | 2 - .../flavours/glitch/locales/whitelist_ro.json | 2 - .../flavours/glitch/locales/whitelist_ru.json | 2 - .../flavours/glitch/locales/whitelist_sa.json | 2 - .../flavours/glitch/locales/whitelist_sc.json | 2 - .../flavours/glitch/locales/whitelist_si.json | 2 - .../flavours/glitch/locales/whitelist_sk.json | 2 - .../flavours/glitch/locales/whitelist_sl.json | 2 - .../flavours/glitch/locales/whitelist_sq.json | 2 - .../glitch/locales/whitelist_sr-Latn.json | 2 - .../flavours/glitch/locales/whitelist_sr.json | 2 - .../flavours/glitch/locales/whitelist_sv.json | 2 - .../glitch/locales/whitelist_szl.json | 2 - .../flavours/glitch/locales/whitelist_ta.json | 2 - .../glitch/locales/whitelist_tai.json | 2 - .../flavours/glitch/locales/whitelist_te.json | 2 - .../flavours/glitch/locales/whitelist_th.json | 2 - .../flavours/glitch/locales/whitelist_tr.json | 2 - .../flavours/glitch/locales/whitelist_tt.json | 2 - .../flavours/glitch/locales/whitelist_ug.json | 2 - .../flavours/glitch/locales/whitelist_uk.json | 2 - .../flavours/glitch/locales/whitelist_ur.json | 2 - .../flavours/glitch/locales/whitelist_vi.json | 2 - .../glitch/locales/whitelist_zgh.json | 2 - .../glitch/locales/whitelist_zh-CN.json | 2 - .../glitch/locales/whitelist_zh-HK.json | 2 - .../glitch/locales/whitelist_zh-TW.json | 2 - .../flavours/glitch/locales/zgh.json | 13 +- .../flavours/glitch/locales/zh-CN.json | 34 + .../flavours/glitch/locales/zh-HK.json | 4 + .../flavours/glitch/locales/zh-TW.json | 4 + .../flavours/glitch/{main.js => main.jsx} | 17 +- .../flavours/glitch/middleware/errors.js | 17 - .../flavours/glitch/middleware/loading_bar.js | 25 - .../flavours/glitch/middleware/sounds.js | 46 - .../glitch/packs/{admin.js => admin.jsx} | 17 +- .../flavours/glitch/packs/common.js | 3 +- app/javascript/flavours/glitch/packs/home.js | 17 +- .../glitch/packs/{public.js => public.jsx} | 85 +- .../flavours/glitch/packs/settings.js | 11 +- app/javascript/flavours/glitch/packs/share.js | 23 - .../flavours/glitch/packs/share.jsx | 27 + .../flavours/glitch/packs/sign_up.js | 16 + app/javascript/flavours/glitch/performance.js | 5 +- app/javascript/flavours/glitch/permissions.js | 4 - app/javascript/flavours/glitch/permissions.ts | 4 + .../glitch/polyfills/base_polyfills.ts | 30 + .../extra_polyfills.ts} | 1 - .../glitch/polyfills/index.ts} | 20 +- .../flavours/glitch/polyfills/intl.ts | 105 + .../flavours/glitch/reducers/accounts.js | 7 +- .../glitch/reducers/accounts_counters.js | 5 +- .../flavours/glitch/reducers/accounts_map.js | 7 +- .../flavours/glitch/reducers/alerts.js | 5 +- .../flavours/glitch/reducers/announcements.js | 5 +- .../flavours/glitch/reducers/compose.js | 65 +- .../flavours/glitch/reducers/contexts.js | 10 +- .../flavours/glitch/reducers/conversations.js | 10 +- .../flavours/glitch/reducers/custom_emojis.js | 5 +- .../flavours/glitch/reducers/domain_lists.js | 5 +- .../flavours/glitch/reducers/dropdown_menu.js | 1 + .../flavours/glitch/reducers/filters.js | 7 +- .../flavours/glitch/reducers/followed_tags.js | 5 +- .../flavours/glitch/reducers/height_cache.js | 3 +- .../flavours/glitch/reducers/history.js | 3 +- .../glitch/reducers/{index.js => index.ts} | 95 +- .../flavours/glitch/reducers/list_adder.js | 3 +- .../flavours/glitch/reducers/list_editor.js | 13 +- .../flavours/glitch/reducers/lists.js | 5 +- .../glitch/reducers/local_settings.js | 6 +- .../flavours/glitch/reducers/markers.js | 7 +- .../glitch/reducers/media_attachments.js | 5 +- .../flavours/glitch/reducers/meta.js | 19 +- .../flavours/glitch/reducers/modal.js | 39 - .../flavours/glitch/reducers/modal.ts | 94 + .../flavours/glitch/reducers/notifications.js | 28 +- .../glitch/reducers/picture_in_picture.js | 2 +- .../glitch/reducers/pinned_accounts_editor.js | 9 +- .../flavours/glitch/reducers/polls.js | 32 +- .../glitch/reducers/push_notifications.js | 7 +- .../flavours/glitch/reducers/relationships.js | 15 +- .../flavours/glitch/reducers/search.js | 15 +- .../flavours/glitch/reducers/server.js | 18 +- .../flavours/glitch/reducers/settings.js | 23 +- .../flavours/glitch/reducers/status_lists.js | 45 +- .../flavours/glitch/reducers/statuses.js | 32 +- .../flavours/glitch/reducers/suggestions.js | 11 +- .../flavours/glitch/reducers/tags.js | 5 +- .../flavours/glitch/reducers/timelines.js | 18 +- .../flavours/glitch/reducers/trends.js | 5 +- .../flavours/glitch/reducers/user_lists.js | 37 +- app/javascript/flavours/glitch/scroll.js | 32 - app/javascript/flavours/glitch/scroll.ts | 48 + .../flavours/glitch/selectors/index.js | 10 +- .../flavours/glitch/store/configureStore.js | 15 - app/javascript/flavours/glitch/store/index.ts | 45 + .../glitch/store/middlewares/errors.ts | 22 + .../glitch/store/middlewares/loading_bar.ts | 45 + .../glitch/store/middlewares/sounds.ts | 62 + app/javascript/flavours/glitch/stream.js | 25 +- .../flavours/glitch/styles/_mixins.scss | 8 +- .../flavours/glitch/styles/about.scss | 4 +- .../flavours/glitch/styles/accessibility.scss | 13 +- .../flavours/glitch/styles/accounts.scss | 40 +- .../flavours/glitch/styles/admin.scss | 148 +- .../flavours/glitch/styles/basics.scss | 100 +- .../glitch/styles/components/about.scss | 6 +- .../glitch/styles/components/accounts.scss | 116 +- .../styles/components/announcements.scss | 16 +- .../glitch/styles/components/columns.scss | 196 +- .../styles/components/compose_form.scss | 199 +- .../glitch/styles/components/directory.scss | 13 +- .../glitch/styles/components/doodle.scss | 32 +- .../glitch/styles/components/drawer.scss | 53 +- .../glitch/styles/components/emoji.scss | 11 +- .../styles/components/emoji_picker.scss | 18 +- .../styles/components/error_boundary.scss | 4 +- .../glitch/styles/components/explore.scss | 4 +- .../glitch/styles/components/index.scss | 1805 +--- .../glitch/styles/components/lists.scss | 4 +- .../styles/components/local_settings.scss | 25 +- .../glitch/styles/components/media.scss | 127 +- .../glitch/styles/components/misc.scss | 1664 +++ .../glitch/styles/components/modal.scss | 104 +- .../styles/components/privacy_policy.scss | 12 +- .../glitch/styles/components/search.scss | 14 +- .../glitch/styles/components/sensitive.scss | 10 +- .../glitch/styles/components/signed_out.scss | 2 +- .../styles/components/single_column.scss | 15 +- .../glitch/styles/components/status.scss | 263 +- .../flavours/glitch/styles/containers.scss | 15 +- .../glitch/styles/contrast/variables.scss | 2 +- .../flavours/glitch/styles/dashboard.scss | 7 +- .../flavours/glitch/styles/forms.scss | 200 +- .../flavours/glitch/styles/index.scss | 1 + .../glitch/styles/mastodon-light/diff.scss | 104 +- .../styles/mastodon-light/variables.scss | 13 + .../flavours/glitch/styles/modal.scss | 8 +- .../flavours/glitch/styles/polls.scss | 24 +- .../flavours/glitch/styles/rich_text.scss | 99 + .../flavours/glitch/styles/rtl.scss | 281 +- .../flavours/glitch/styles/statuses.scss | 15 +- .../flavours/glitch/styles/tables.scss | 24 +- .../flavours/glitch/styles/variables.scss | 65 +- .../flavours/glitch/styles/widgets.scss | 10 +- app/javascript/flavours/glitch/theme.yml | 13 +- .../flavours/glitch/types/resources.ts | 54 + app/javascript/flavours/glitch/types/util.ts | 1 + .../glitch/utils/base64.ts} | 2 +- .../flavours/glitch/utils/dom_helpers.js | 7 - .../flavours/glitch/utils/filters.js | 16 - .../flavours/glitch/utils/filters.ts | 16 + .../flavours/glitch/utils/hashtag.js | 8 +- .../glitch/utils/{icons.js => icons.jsx} | 4 - .../flavours/glitch/utils/log_out.js | 1 + .../flavours/glitch/utils/notifications.js | 2 +- .../glitch/utils/{numbers.js => numbers.ts} | 50 +- .../glitch/utils/privacy_preference.js | 2 +- .../flavours/glitch/utils/react_helpers.js | 2 +- .../flavours/glitch/utils/resize_image.js | 4 +- .../flavours/glitch/utils/scrollbar.js | 4 +- app/javascript/flavours/glitch/uuid.js | 3 - app/javascript/flavours/glitch/uuid.ts | 9 + app/javascript/flavours/vanilla/theme.yml | 15 +- app/javascript/hooks/useHovering.ts | 17 + .../images/elephant_ui_conversation.svg | 57 + app/javascript/images/friends-cropped.png | Bin 0 -> 193366 bytes app/javascript/locales/index.js | 9 - app/javascript/locales/locale-data/README.md | 221 - app/javascript/locales/locale-data/oc.js | 108 - app/javascript/mastodon/actions/accounts.js | 1 + .../mastodon/actions/announcements.js | 1 + app/javascript/mastodon/actions/app.js | 17 - app/javascript/mastodon/actions/app.ts | 12 + app/javascript/mastodon/actions/blocks.js | 3 +- app/javascript/mastodon/actions/bookmarks.js | 1 + app/javascript/mastodon/actions/boosts.js | 5 +- app/javascript/mastodon/actions/compose.js | 113 +- .../mastodon/actions/conversations.js | 1 + app/javascript/mastodon/actions/directory.js | 3 +- app/javascript/mastodon/actions/favourites.js | 1 + app/javascript/mastodon/actions/filters.js | 10 +- app/javascript/mastodon/actions/history.js | 1 + .../mastodon/actions/importer/normalizer.js | 41 +- .../mastodon/actions/interactions.js | 1 + app/javascript/mastodon/actions/lists.js | 7 +- app/javascript/mastodon/actions/markers.js | 10 +- app/javascript/mastodon/actions/modal.js | 18 - app/javascript/mastodon/actions/modal.ts | 17 + app/javascript/mastodon/actions/mutes.js | 3 +- .../mastodon/actions/notifications.js | 18 +- .../mastodon/actions/picture_in_picture.js | 3 +- .../mastodon/actions/pin_statuses.js | 4 +- app/javascript/mastodon/actions/polls.js | 1 + .../actions/push_notifications/index.js | 2 +- .../actions/push_notifications/registerer.js | 9 +- app/javascript/mastodon/actions/reports.js | 10 +- app/javascript/mastodon/actions/search.js | 51 +- app/javascript/mastodon/actions/server.js | 40 + app/javascript/mastodon/actions/settings.js | 4 +- app/javascript/mastodon/actions/statuses.js | 7 +- app/javascript/mastodon/actions/store.js | 1 + app/javascript/mastodon/actions/streaming.js | 68 +- .../mastodon/actions/suggestions.js | 3 +- app/javascript/mastodon/actions/tags.js | 14 +- app/javascript/mastodon/actions/timelines.js | 10 +- app/javascript/mastodon/actions/trends.js | 1 + app/javascript/mastodon/api.js | 5 +- app/javascript/mastodon/base_polyfills.js | 47 - .../blurhash.js => mastodon/blurhash.ts} | 10 +- app/javascript/mastodon/common.js | 2 +- .../compare_id.js => mastodon/compare_id.ts} | 4 +- ...s.snap => autosuggest_emoji-test.jsx.snap} | 0 ...atar-test.js.snap => avatar-test.jsx.snap} | 0 ...t.js.snap => avatar_overlay-test.jsx.snap} | 6 +- ...tton-test.js.snap => button-test.jsx.snap} | 0 ...est.js.snap => display_name-test.jsx.snap} | 0 ...oji-test.js => autosuggest_emoji-test.jsx} | 2 +- .../{avatar-test.js => avatar-test.jsx} | 7 +- ...verlay-test.js => avatar_overlay-test.jsx} | 7 +- .../{button-test.js => button-test.jsx} | 2 +- ...lay_name-test.js => display_name-test.jsx} | 7 +- app/javascript/mastodon/components/account.js | 157 - .../mastodon/components/account.jsx | 190 + .../admin/{Counter.js => Counter.jsx} | 26 +- .../admin/{Dimension.js => Dimension.jsx} | 14 +- .../components/admin/ImpactReport.jsx | 91 + ...onSelector.js => ReportReasonSelector.jsx} | 20 +- .../admin/{Retention.js => Retention.jsx} | 9 +- .../admin/{Trends.js => Trends.jsx} | 9 +- .../mastodon/components/animated_number.js | 76 - .../mastodon/components/animated_number.tsx | 81 + .../components/attachment_list.jsx} | 11 +- ...suggest_emoji.js => autosuggest_emoji.jsx} | 8 +- .../components/autosuggest_hashtag.js | 42 - .../components/autosuggest_hashtag.tsx | 42 + ...suggest_input.js => autosuggest_input.jsx} | 39 +- ...t_textarea.js => autosuggest_textarea.jsx} | 42 +- app/javascript/mastodon/components/avatar.js | 62 - app/javascript/mastodon/components/avatar.tsx | 47 + ...atar_composite.js => avatar_composite.jsx} | 13 +- .../mastodon/components/avatar_overlay.js | 51 - .../mastodon/components/avatar_overlay.tsx | 54 + .../mastodon/components/blurhash.js | 65 - .../mastodon/components/blurhash.tsx | 48 + .../components/{button.js => button.jsx} | 9 +- app/javascript/mastodon/components/check.js | 9 - app/javascript/mastodon/components/check.tsx | 13 + .../mastodon/components/circular_progress.tsx | 27 + .../components/{column.js => column.jsx} | 20 +- .../components/column_back_button.jsx} | 34 +- ...on_slim.js => column_back_button_slim.jsx} | 7 +- .../{column_header.js => column_header.jsx} | 42 +- .../{common_counter.js => common_counter.jsx} | 2 - ...sable_banner.js => dismissable_banner.jsx} | 14 +- .../mastodon/components/display_name.js | 79 - .../mastodon/components/display_name.tsx | 121 + app/javascript/mastodon/components/domain.js | 42 - app/javascript/mastodon/components/domain.tsx | 44 + .../components/dropdown_menu.jsx} | 79 +- .../containers/dropdown_menu_container.js | 1 + .../edited_timestamp/{index.js => index.jsx} | 29 +- .../mastodon/components/empty_account.tsx | 33 + .../{error_boundary.js => error_boundary.jsx} | 14 +- app/javascript/mastodon/components/gifv.js | 73 - app/javascript/mastodon/components/gifv.tsx | 70 + .../components/{hashtag.js => hashtag.jsx} | 35 +- app/javascript/mastodon/components/icon.js | 21 - app/javascript/mastodon/components/icon.tsx | 20 + .../{icon_button.js => icon_button.tsx} | 106 +- .../mastodon/components/icon_with_badge.js | 22 - .../mastodon/components/icon_with_badge.tsx | 24 + app/javascript/mastodon/components/image.js | 33 - .../{inline_account.js => inline_account.jsx} | 11 +- ...e.js => intersection_observer_article.jsx} | 27 +- .../mastodon/components/load_gap.js | 34 - .../mastodon/components/load_gap.tsx | 34 + .../mastodon/components/load_more.js | 27 - .../mastodon/components/load_more.tsx | 24 + .../mastodon/components/load_pending.js | 22 - .../mastodon/components/load_pending.tsx | 18 + .../mastodon/components/loading_indicator.js | 32 - .../mastodon/components/loading_indicator.tsx | 7 + app/javascript/mastodon/components/logo.js | 10 - app/javascript/mastodon/components/logo.tsx | 12 + ...a_attachments.js => media_attachments.jsx} | 25 +- .../{media_gallery.js => media_gallery.jsx} | 132 +- .../mastodon/components/missing_indicator.js | 29 - .../{modal_root.js => modal_root.jsx} | 21 +- ...gation_portal.js => navigation_portal.jsx} | 26 +- .../components/not_signed_in_indicator.js | 12 - .../components/not_signed_in_indicator.tsx | 12 + .../picture_in_picture_placeholder.js | 69 - .../picture_in_picture_placeholder.jsx | 33 + .../mastodon/components/{poll.js => poll.jsx} | 66 +- .../mastodon/components/radio_button.js | 35 - .../mastodon/components/radio_button.tsx | 33 + ...ndicator.js => regeneration_indicator.jsx} | 2 +- .../mastodon/components/relative_timestamp.js | 199 - .../components/relative_timestamp.tsx | 282 + ...scrollable_list.js => scrollable_list.jsx} | 74 +- .../{server_banner.js => server_banner.jsx} | 22 +- .../mastodon/components/server_hero_image.tsx | 35 + .../{short_number.js => short_number.jsx} | 14 +- .../mastodon/components/skeleton.js | 11 - .../mastodon/components/skeleton.tsx | 10 + .../components/{status.js => status.jsx} | 165 +- ...us_action_bar.js => status_action_bar.jsx} | 96 +- .../{status_content.js => status_content.jsx} | 83 +- .../{status_list.js => status_list.jsx} | 32 +- .../mastodon/components/timeline_hint.js | 18 - .../mastodon/components/timeline_hint.tsx | 25 + .../mastodon/components/verified_badge.tsx | 27 + ...unt_container.js => account_container.jsx} | 20 +- .../mastodon/containers/admin_component.js | 26 - .../mastodon/containers/admin_component.jsx | 22 + .../mastodon/containers/compose_container.js | 41 - .../mastodon/containers/compose_container.jsx | 31 + .../mastodon/containers/domain_container.jsx | 36 + .../containers/dropdown_menu_container.js | 24 +- ...intersection_observer_article_container.js | 3 +- .../containers/{mastodon.js => mastodon.jsx} | 29 +- ...media_container.js => media_container.jsx} | 62 +- .../mastodon/containers/poll_container.js | 3 +- ...atus_container.js => status_container.jsx} | 134 +- .../features/about/{index.js => index.jsx} | 48 +- .../{account_note.js => account_note.jsx} | 28 +- .../{featured_tags.js => featured_tags.jsx} | 9 +- .../components/follow_request_note.jsx} | 7 +- .../components/{header.js => header.jsx} | 128 +- .../containers/account_note_container.js | 2 + .../containers/featured_tags_container.js | 8 +- .../follow_request_note_container.js | 4 +- .../account/{navigation.js => navigation.jsx} | 9 +- .../{media_item.js => media_item.jsx} | 27 +- .../account_gallery/{index.js => index.jsx} | 71 +- .../components/{header.js => header.jsx} | 50 +- ...count_hint.js => limited_account_hint.jsx} | 16 +- .../components/memorial_note.jsx | 11 + .../{moved_note.js => moved_note.jsx} | 12 +- ...ader_container.js => header_container.jsx} | 92 +- .../account_timeline/{index.js => index.jsx} | 49 +- .../features/audio/{index.js => index.jsx} | 137 +- .../features/blocks/{index.js => index.jsx} | 29 +- .../{index.js => index.jsx} | 29 +- .../{index.js => index.jsx} | 10 +- ...column_settings.js => column_settings.jsx} | 12 +- .../containers/column_settings_container.js | 5 +- .../{index.js => index.jsx} | 47 +- .../{action_bar.js => action_bar.jsx} | 20 +- ...est_account.js => autosuggest_account.jsx} | 6 +- ...acter_counter.js => character_counter.jsx} | 5 +- .../{compose_form.js => compose_form.jsx} | 151 +- ..._dropdown.js => emoji_picker_dropdown.jsx} | 80 +- ...uage_dropdown.js => language_dropdown.jsx} | 63 +- .../{navigation_bar.js => navigation_bar.jsx} | 16 +- .../{poll_button.js => poll_button.jsx} | 14 +- .../{poll_form.js => poll_form.jsx} | 43 +- ...ivacy_dropdown.js => privacy_dropdown.jsx} | 63 +- ...reply_indicator.js => reply_indicator.jsx} | 20 +- .../features/compose/components/search.js | 147 - .../features/compose/components/search.jsx | 339 + .../{search_results.js => search_results.jsx} | 22 +- ...xt_icon_button.js => text_icon_button.jsx} | 4 +- .../components/{upload.js => upload.jsx} | 27 +- .../{upload_button.js => upload_button.jsx} | 20 +- .../{upload_form.js => upload_form.jsx} | 6 +- ...upload_progress.js => upload_progress.jsx} | 14 +- .../components/{warning.js => warning.jsx} | 8 +- .../autosuggest_account_container.js | 3 +- .../containers/compose_form_container.js | 4 +- .../emoji_picker_dropdown_container.js | 10 +- .../containers/language_dropdown_container.js | 9 +- .../containers/navigation_container.js | 22 +- .../containers/poll_button_container.js | 3 +- .../compose/containers/poll_form_container.js | 9 +- .../containers/privacy_dropdown_container.js | 13 +- .../containers/reply_indicator_container.js | 1 + .../compose/containers/search_container.js | 24 +- .../containers/search_results_container.js | 6 +- ...iner.js => sensitive_button_container.jsx} | 16 +- .../containers/spoiler_button_container.js | 8 +- .../containers/upload_button_container.js | 3 +- .../compose/containers/upload_container.js | 4 +- .../containers/upload_form_container.js | 1 + .../containers/upload_progress_container.js | 1 + ...ing_container.js => warning_container.jsx} | 35 +- .../features/compose/{index.js => index.jsx} | 60 +- .../mastodon/features/compose/util/counter.js | 2 +- .../features/compose/util/url_regex.js | 2 +- .../{conversation.js => conversation.jsx} | 51 +- ...sations_list.js => conversations_list.jsx} | 22 +- .../containers/conversation_container.js | 22 +- .../conversations_list_container.js | 3 +- .../direct_timeline/{index.js => index.jsx} | 31 +- .../{account_card.js => account_card.jsx} | 75 +- .../directory/{index.js => index.jsx} | 47 +- .../domain_blocks/{index.js => index.jsx} | 34 +- .../features/emoji/__tests__/emoji-test.js | 4 +- .../emoji/__tests__/emoji_index-test.js | 3 +- .../mastodon/features/emoji/emoji.js | 113 +- .../features/emoji/emoji_compressed.d.ts | 51 + .../features/emoji/emoji_compressed.js | 21 +- .../features/emoji/emoji_mart_data_light.js | 41 - .../features/emoji/emoji_mart_data_light.ts | 52 + .../features/emoji/emoji_mart_search_light.js | 2 +- .../mastodon/features/emoji/emoji_picker.js | 2 +- .../emoji/emoji_unicode_mapping_light.js | 14 +- .../mastodon/features/emoji/emoji_utils.js | 14 +- .../features/emoji/unicode_to_filename.js | 3 + .../features/emoji/unicode_to_unified_name.js | 3 + .../components/{story.js => story.jsx} | 18 +- .../features/explore/{index.js => index.jsx} | 52 +- .../features/explore/{links.js => links.jsx} | 23 +- .../explore/{results.js => results.jsx} | 30 +- .../explore/{statuses.js => statuses.jsx} | 24 +- .../{suggestions.js => suggestions.jsx} | 20 +- .../features/explore/{tags.js => tags.jsx} | 22 +- .../{index.js => index.jsx} | 29 +- .../favourites/{index.js => index.jsx} | 27 +- ...added_to_filter.js => added_to_filter.jsx} | 28 +- .../{select_filter.js => select_filter.jsx} | 40 +- .../mastodon/features/firehose/index.jsx | 211 + .../components/account.js | 85 - .../features/follow_recommendations/index.js | 116 - ...unt_authorize.js => account_authorize.jsx} | 18 +- .../containers/account_authorize_container.js | 3 +- .../follow_requests/{index.js => index.jsx} | 39 +- .../followed_tags/{index.js => index.jsx} | 24 +- .../followers/{index.js => index.jsx} | 35 +- .../following/{index.js => index.jsx} | 35 +- .../features/generic_not_found/index.js | 11 - .../{announcements.js => announcements.jsx} | 62 +- .../components/{trends.js => trends.jsx} | 11 +- .../containers/announcements_container.js | 10 +- .../containers/trends_container.js | 2 + .../getting_started/{index.js => index.jsx} | 35 +- .../components/column_settings.jsx} | 21 +- .../containers/column_settings_container.js | 3 +- .../hashtag_timeline/{index.js => index.jsx} | 61 +- ...column_settings.js => column_settings.jsx} | 12 +- .../components/explore_prompt.jsx | 26 + .../containers/column_settings_container.js | 3 +- .../home_timeline/{index.js => index.jsx} | 94 +- .../interaction_modal/{index.js => index.jsx} | 42 +- .../{index.js => index.jsx} | 16 +- .../list_adder/components/account.jsx} | 20 +- .../components/{list.js => list.jsx} | 19 +- .../features/list_adder/index.jsx} | 21 +- .../components/{account.js => account.jsx} | 22 +- .../{edit_list_form.js => edit_list_form.jsx} | 23 +- .../components/{search.js => search.jsx} | 27 +- .../list_editor/{index.js => index.jsx} | 28 +- .../list_timeline/{index.js => index.jsx} | 98 +- .../{new_list_form.js => new_list_form.jsx} | 21 +- .../features/lists/{index.js => index.jsx} | 18 +- .../features/mutes/{index.js => index.jsx} | 34 +- ...lumn_button.js => clear_column_button.jsx} | 12 +- ...column_settings.js => column_settings.jsx} | 14 +- .../notifications/components/filter_bar.jsx} | 11 +- .../{follow_request.js => follow_request.jsx} | 22 +- ..._button.js => grant_permission_button.jsx} | 7 +- .../{notification.js => notification.jsx} | 88 +- ...js => notifications_permission_banner.jsx} | 27 +- .../components/{report.js => report.jsx} | 25 +- .../{setting_toggle.js => setting_toggle.jsx} | 10 +- .../containers/column_settings_container.js | 26 +- .../containers/filter_bar_container.js | 3 +- .../containers/follow_request_container.js | 6 +- .../containers/notification_container.js | 7 +- .../notifications/{index.js => index.jsx} | 63 +- .../components/arrow_small_right.jsx | 7 + .../components/progress_indicator.jsx | 27 + .../features/onboarding/components/step.jsx | 50 + .../mastodon/features/onboarding/follows.jsx | 80 + .../mastodon/features/onboarding/index.jsx | 154 + .../mastodon/features/onboarding/share.jsx | 200 + .../components/{footer.js => footer.jsx} | 75 +- .../components/{header.js => header.jsx} | 23 +- .../{index.js => index.jsx} | 20 +- .../pinned_statuses/{index.js => index.jsx} | 29 +- .../privacy_policy/{index.js => index.jsx} | 16 +- ...column_settings.js => column_settings.jsx} | 12 +- .../containers/column_settings_container.js | 5 +- .../public_timeline/{index.js => index.jsx} | 46 +- .../features/reblogs/{index.js => index.jsx} | 34 +- .../report/{category.js => category.jsx} | 24 +- .../report/{comment.js => comment.jsx} | 20 +- .../components/{option.js => option.jsx} | 16 +- ...atus_check_box.js => status_check_box.jsx} | 29 +- .../containers/status_check_box_container.js | 4 +- .../features/report/{rules.js => rules.jsx} | 17 +- .../report/{statuses.js => statuses.jsx} | 22 +- .../features/report/{thanks.js => thanks.jsx} | 22 +- .../compose/{index.js => index.jsx} | 9 +- .../{action_bar.js => action_bar.jsx} | 93 +- .../status/components/{card.js => card.jsx} | 95 +- ...detailed_status.js => detailed_status.jsx} | 95 +- .../containers/detailed_status_container.js | 65 +- .../features/status/{index.js => index.jsx} | 292 +- .../{index.js => index.jsx} | 24 +- .../{column-test.js => column-test.jsx} | 2 +- .../{actions_modal.js => actions_modal.jsx} | 12 +- .../{audio_modal.js => audio_modal.jsx} | 23 +- .../{block_modal.js => block_modal.jsx} | 37 +- .../{boost_modal.js => boost_modal.jsx} | 44 +- .../ui/components/{bundle.js => bundle.jsx} | 18 +- ...olumn_error.js => bundle_column_error.jsx} | 28 +- ..._modal_error.js => bundle_modal_error.jsx} | 11 +- .../ui/components/{column.js => column.jsx} | 21 +- .../{column_header.js => column_header.jsx} | 12 +- .../{column_link.js => column_link.jsx} | 7 +- .../{column_loading.js => column_loading.jsx} | 4 +- ...mn_subheading.js => column_subheading.jsx} | 1 - .../{columns_area.js => columns_area.jsx} | 34 +- ...ory_modal.js => compare_history_modal.jsx} | 41 +- .../{compose_panel.js => compose_panel.jsx} | 32 +- ...mation_modal.js => confirmation_modal.jsx} | 17 +- ..._banner.js => disabled_account_banner.jsx} | 33 +- .../ui/components/drawer_loading.jsx} | 2 - .../{embed_modal.js => embed_modal.jsx} | 19 +- .../{filter_modal.js => filter_modal.jsx} | 18 +- ...l_point_modal.js => focal_point_modal.jsx} | 89 +- ...ink.js => follow_requests_column_link.jsx} | 21 +- .../mastodon/features/ui/components/header.js | 87 - .../features/ui/components/header.jsx | 116 + .../{image_loader.js => image_loader.jsx} | 20 +- .../features/ui/components/image_modal.jsx} | 15 +- .../{link_footer.js => link_footer.jsx} | 47 +- .../{list_panel.js => list_panel.jsx} | 13 +- .../{media_modal.js => media_modal.jsx} | 61 +- .../features/ui/components/modal_loading.jsx} | 4 +- .../{modal_root.js => modal_root.jsx} | 48 +- .../{mute_modal.js => mute_modal.jsx} | 33 +- ...vigation_panel.js => navigation_panel.jsx} | 41 +- .../components/notifications_counter_icon.js | 3 +- .../{report_modal.js => report_modal.jsx} | 28 +- .../features/ui/components/sign_in_banner.js | 40 - .../features/ui/components/sign_in_banner.jsx | 45 + .../{upload_area.js => upload_area.jsx} | 13 +- .../{video_modal.js => video_modal.jsx} | 32 +- .../ui/components/zoomable_image.jsx} | 48 +- .../ui/containers/bundle_container.js | 3 +- .../ui/containers/columns_area_container.js | 1 + .../ui/containers/loading_bar_container.js | 1 + .../features/ui/containers/modal_container.js | 21 +- .../ui/containers/notifications_container.js | 3 + .../ui/containers/status_list_container.js | 9 +- .../features/ui/{index.js => index.jsx} | 153 +- .../features/ui/util/async-components.js | 12 +- .../features/ui/util/optional_motion.js | 6 +- ...er_helpers.js => react_router_helpers.jsx} | 19 +- .../features/ui/util/reduced_motion.jsx} | 9 +- .../features/video/{index.js => index.jsx} | 133 +- app/javascript/mastodon/initial_state.js | 7 +- .../mastodon/{is_mobile.js => is_mobile.ts} | 23 +- app/javascript/mastodon/locales/af.json | 85 +- app/javascript/mastodon/locales/an.json | 93 +- app/javascript/mastodon/locales/ar.json | 101 +- app/javascript/mastodon/locales/ast.json | 237 +- app/javascript/mastodon/locales/be.json | 89 +- app/javascript/mastodon/locales/bg.json | 129 +- app/javascript/mastodon/locales/bn.json | 183 +- app/javascript/mastodon/locales/br.json | 85 +- app/javascript/mastodon/locales/bs.json | 85 +- app/javascript/mastodon/locales/ca.json | 147 +- app/javascript/mastodon/locales/ckb.json | 199 +- app/javascript/mastodon/locales/co.json | 83 +- app/javascript/mastodon/locales/cs.json | 79 +- app/javascript/mastodon/locales/cy.json | 159 +- app/javascript/mastodon/locales/da.json | 79 +- app/javascript/mastodon/locales/de.json | 181 +- .../mastodon/locales/defaultMessages.json | 4317 -------- app/javascript/mastodon/locales/el.json | 573 +- app/javascript/mastodon/locales/en-GB.json | 91 +- app/javascript/mastodon/locales/en.json | 129 +- app/javascript/mastodon/locales/eo.json | 159 +- app/javascript/mastodon/locales/es-AR.json | 89 +- app/javascript/mastodon/locales/es-MX.json | 81 +- app/javascript/mastodon/locales/es.json | 99 +- app/javascript/mastodon/locales/et.json | 105 +- app/javascript/mastodon/locales/eu.json | 91 +- app/javascript/mastodon/locales/fa.json | 113 +- app/javascript/mastodon/locales/fi.json | 257 +- app/javascript/mastodon/locales/fo.json | 83 +- app/javascript/mastodon/locales/fr-QC.json | 83 +- app/javascript/mastodon/locales/fr.json | 121 +- app/javascript/mastodon/locales/fy.json | 79 +- app/javascript/mastodon/locales/ga.json | 123 +- app/javascript/mastodon/locales/gd.json | 95 +- app/javascript/mastodon/locales/gl.json | 83 +- .../mastodon/locales/global_locale.ts | 22 + app/javascript/mastodon/locales/he.json | 103 +- app/javascript/mastodon/locales/hi.json | 97 +- app/javascript/mastodon/locales/hr.json | 89 +- app/javascript/mastodon/locales/hu.json | 317 +- app/javascript/mastodon/locales/hy.json | 213 +- app/javascript/mastodon/locales/id.json | 97 +- app/javascript/mastodon/locales/ig.json | 85 +- app/javascript/mastodon/locales/index.js | 1 - app/javascript/mastodon/locales/index.ts | 5 + .../mastodon/locales/intl_provider.tsx | 57 + app/javascript/mastodon/locales/io.json | 79 +- app/javascript/mastodon/locales/is.json | 79 +- app/javascript/mastodon/locales/it.json | 83 +- app/javascript/mastodon/locales/ja.json | 95 +- app/javascript/mastodon/locales/ka.json | 85 +- app/javascript/mastodon/locales/kab.json | 93 +- app/javascript/mastodon/locales/kk.json | 161 +- app/javascript/mastodon/locales/kn.json | 85 +- app/javascript/mastodon/locales/ko.json | 145 +- app/javascript/mastodon/locales/ku.json | 87 +- app/javascript/mastodon/locales/kw.json | 85 +- app/javascript/mastodon/locales/la.json | 91 +- .../mastodon/locales/load_locale.ts | 29 + .../mastodon/locales/locale-data/co.js | 108 - .../mastodon/locales/locale-data/sa.js | 97 - app/javascript/mastodon/locales/lt.json | 87 +- app/javascript/mastodon/locales/lv.json | 93 +- app/javascript/mastodon/locales/mk.json | 85 +- app/javascript/mastodon/locales/ml.json | 85 +- app/javascript/mastodon/locales/mr.json | 89 +- app/javascript/mastodon/locales/ms.json | 87 +- app/javascript/mastodon/locales/my.json | 1139 ++- app/javascript/mastodon/locales/nl.json | 91 +- app/javascript/mastodon/locales/nn.json | 83 +- app/javascript/mastodon/locales/no.json | 85 +- app/javascript/mastodon/locales/oc.json | 89 +- app/javascript/mastodon/locales/pa.json | 85 +- app/javascript/mastodon/locales/pl.json | 150 +- app/javascript/mastodon/locales/pt-BR.json | 95 +- app/javascript/mastodon/locales/pt-PT.json | 85 +- app/javascript/mastodon/locales/ro.json | 87 +- app/javascript/mastodon/locales/ru.json | 85 +- app/javascript/mastodon/locales/sa.json | 789 +- app/javascript/mastodon/locales/sc.json | 83 +- app/javascript/mastodon/locales/sco.json | 79 +- app/javascript/mastodon/locales/si.json | 81 +- app/javascript/mastodon/locales/sk.json | 125 +- app/javascript/mastodon/locales/sl.json | 79 +- app/javascript/mastodon/locales/sq.json | 97 +- app/javascript/mastodon/locales/sr-Latn.json | 193 +- app/javascript/mastodon/locales/sr.json | 205 +- app/javascript/mastodon/locales/sv.json | 101 +- app/javascript/mastodon/locales/szl.json | 125 +- app/javascript/mastodon/locales/ta.json | 83 +- app/javascript/mastodon/locales/tai.json | 85 +- app/javascript/mastodon/locales/te.json | 85 +- app/javascript/mastodon/locales/th.json | 107 +- app/javascript/mastodon/locales/tr.json | 87 +- app/javascript/mastodon/locales/tt.json | 551 +- app/javascript/mastodon/locales/ug.json | 85 +- app/javascript/mastodon/locales/uk.json | 79 +- app/javascript/mastodon/locales/ur.json | 85 +- app/javascript/mastodon/locales/uz.json | 692 ++ app/javascript/mastodon/locales/vi.json | 75 +- .../mastodon/locales/whitelist_af.json | 2 - .../mastodon/locales/whitelist_an.json | 2 - .../mastodon/locales/whitelist_ar.json | 2 - .../mastodon/locales/whitelist_ast.json | 2 - .../mastodon/locales/whitelist_be.json | 2 - .../mastodon/locales/whitelist_bg.json | 2 - .../mastodon/locales/whitelist_bn.json | 2 - .../mastodon/locales/whitelist_br.json | 2 - .../mastodon/locales/whitelist_bs.json | 2 - .../mastodon/locales/whitelist_ca.json | 2 - .../mastodon/locales/whitelist_ckb.json | 2 - .../mastodon/locales/whitelist_co.json | 2 - .../mastodon/locales/whitelist_cs.json | 2 - .../mastodon/locales/whitelist_cy.json | 2 - .../mastodon/locales/whitelist_da.json | 2 - .../mastodon/locales/whitelist_de.json | 5 - .../mastodon/locales/whitelist_el.json | 2 - .../mastodon/locales/whitelist_en-GB.json | 2 - .../mastodon/locales/whitelist_en.json | 2 - .../mastodon/locales/whitelist_eo.json | 2 - .../mastodon/locales/whitelist_es-AR.json | 2 - .../mastodon/locales/whitelist_es-MX.json | 2 - .../mastodon/locales/whitelist_es.json | 2 - .../mastodon/locales/whitelist_et.json | 2 - .../mastodon/locales/whitelist_eu.json | 2 - .../mastodon/locales/whitelist_fa.json | 2 - .../mastodon/locales/whitelist_fi.json | 2 - .../mastodon/locales/whitelist_fo.json | 2 - .../mastodon/locales/whitelist_fr-QC.json | 2 - .../mastodon/locales/whitelist_fr.json | 2 - .../mastodon/locales/whitelist_fy.json | 2 - .../mastodon/locales/whitelist_ga.json | 2 - .../mastodon/locales/whitelist_gd.json | 2 - .../mastodon/locales/whitelist_gl.json | 2 - .../mastodon/locales/whitelist_he.json | 2 - .../mastodon/locales/whitelist_hi.json | 2 - .../mastodon/locales/whitelist_hr.json | 2 - .../mastodon/locales/whitelist_hu.json | 2 - .../mastodon/locales/whitelist_hy.json | 2 - .../mastodon/locales/whitelist_id.json | 2 - .../mastodon/locales/whitelist_ig.json | 2 - .../mastodon/locales/whitelist_io.json | 2 - .../mastodon/locales/whitelist_is.json | 2 - .../mastodon/locales/whitelist_it.json | 2 - .../mastodon/locales/whitelist_ja.json | 2 - .../mastodon/locales/whitelist_ka.json | 2 - .../mastodon/locales/whitelist_kab.json | 2 - .../mastodon/locales/whitelist_kk.json | 2 - .../mastodon/locales/whitelist_kn.json | 2 - .../mastodon/locales/whitelist_ko.json | 2 - .../mastodon/locales/whitelist_ku.json | 2 - .../mastodon/locales/whitelist_kw.json | 2 - .../mastodon/locales/whitelist_la.json | 2 - .../mastodon/locales/whitelist_lt.json | 2 - .../mastodon/locales/whitelist_lv.json | 2 - .../mastodon/locales/whitelist_mk.json | 2 - .../mastodon/locales/whitelist_ml.json | 2 - .../mastodon/locales/whitelist_mr.json | 2 - .../mastodon/locales/whitelist_ms.json | 2 - .../mastodon/locales/whitelist_my.json | 2 - .../mastodon/locales/whitelist_nl.json | 2 - .../mastodon/locales/whitelist_nn.json | 2 - .../mastodon/locales/whitelist_no.json | 2 - .../mastodon/locales/whitelist_oc.json | 2 - .../mastodon/locales/whitelist_pa.json | 2 - .../mastodon/locales/whitelist_pl.json | 2 - .../mastodon/locales/whitelist_pt-BR.json | 2 - .../mastodon/locales/whitelist_pt-PT.json | 2 - .../mastodon/locales/whitelist_ro.json | 2 - .../mastodon/locales/whitelist_ru.json | 2 - .../mastodon/locales/whitelist_sa.json | 2 - .../mastodon/locales/whitelist_sc.json | 2 - .../mastodon/locales/whitelist_sco.json | 2 - .../mastodon/locales/whitelist_si.json | 2 - .../mastodon/locales/whitelist_sk.json | 2 - .../mastodon/locales/whitelist_sl.json | 2 - .../mastodon/locales/whitelist_sq.json | 2 - .../mastodon/locales/whitelist_sr-Latn.json | 2 - .../mastodon/locales/whitelist_sr.json | 2 - .../mastodon/locales/whitelist_sv.json | 2 - .../mastodon/locales/whitelist_szl.json | 2 - .../mastodon/locales/whitelist_ta.json | 2 - .../mastodon/locales/whitelist_tai.json | 2 - .../mastodon/locales/whitelist_te.json | 2 - .../mastodon/locales/whitelist_th.json | 2 - .../mastodon/locales/whitelist_tr.json | 2 - .../mastodon/locales/whitelist_tt.json | 2 - .../mastodon/locales/whitelist_ug.json | 2 - .../mastodon/locales/whitelist_uk.json | 2 - .../mastodon/locales/whitelist_ur.json | 2 - .../mastodon/locales/whitelist_vi.json | 2 - .../mastodon/locales/whitelist_zgh.json | 2 - .../mastodon/locales/whitelist_zh-CN.json | 2 - .../mastodon/locales/whitelist_zh-HK.json | 2 - .../mastodon/locales/whitelist_zh-TW.json | 2 - app/javascript/mastodon/locales/zgh.json | 85 +- app/javascript/mastodon/locales/zh-CN.json | 111 +- app/javascript/mastodon/locales/zh-HK.json | 85 +- app/javascript/mastodon/locales/zh-TW.json | 209 +- app/javascript/mastodon/{main.js => main.jsx} | 17 +- app/javascript/mastodon/middleware/errors.js | 17 - .../mastodon/middleware/loading_bar.js | 25 - app/javascript/mastodon/middleware/sounds.js | 46 - app/javascript/mastodon/performance.js | 5 +- app/javascript/mastodon/permissions.js | 4 - app/javascript/mastodon/permissions.ts | 4 + .../mastodon/polyfills/base_polyfills.ts | 30 + .../extra_polyfills.ts} | 1 - .../polyfills/index.ts} | 20 +- app/javascript/mastodon/polyfills/intl.ts | 105 + app/javascript/mastodon/reducers/accounts.js | 5 +- .../mastodon/reducers/accounts_counters.js | 17 +- .../mastodon/reducers/accounts_map.js | 5 +- app/javascript/mastodon/reducers/alerts.js | 3 +- .../mastodon/reducers/announcements.js | 3 +- app/javascript/mastodon/reducers/compose.js | 29 +- app/javascript/mastodon/reducers/contexts.js | 5 +- .../mastodon/reducers/conversations.js | 8 +- .../mastodon/reducers/custom_emojis.js | 3 +- .../mastodon/reducers/domain_lists.js | 3 +- .../mastodon/reducers/dropdown_menu.js | 1 + app/javascript/mastodon/reducers/filters.js | 5 +- .../mastodon/reducers/followed_tags.js | 5 +- .../mastodon/reducers/height_cache.js | 1 + app/javascript/mastodon/reducers/history.js | 3 +- .../mastodon/reducers/{index.js => index.ts} | 97 +- .../mastodon/reducers/list_adder.js | 1 + .../mastodon/reducers/list_editor.js | 3 + app/javascript/mastodon/reducers/lists.js | 3 +- app/javascript/mastodon/reducers/markers.js | 5 +- .../mastodon/reducers/media_attachments.js | 3 +- app/javascript/mastodon/reducers/meta.js | 9 +- .../mastodon/reducers/missed_updates.js | 21 - .../mastodon/reducers/missed_updates.ts | 33 + app/javascript/mastodon/reducers/modal.js | 39 - app/javascript/mastodon/reducers/modal.ts | 94 + .../mastodon/reducers/notifications.js | 38 +- .../mastodon/reducers/picture_in_picture.js | 1 + app/javascript/mastodon/reducers/polls.js | 32 +- .../mastodon/reducers/push_notifications.js | 5 +- .../mastodon/reducers/relationships.js | 12 +- app/javascript/mastodon/reducers/search.js | 20 +- app/javascript/mastodon/reducers/server.js | 18 +- app/javascript/mastodon/reducers/settings.js | 15 +- .../mastodon/reducers/status_lists.js | 43 +- app/javascript/mastodon/reducers/statuses.js | 31 +- .../mastodon/reducers/suggestions.js | 9 +- app/javascript/mastodon/reducers/tags.js | 3 +- app/javascript/mastodon/reducers/timelines.js | 15 +- app/javascript/mastodon/reducers/trends.js | 3 +- .../mastodon/reducers/user_lists.js | 42 +- app/javascript/mastodon/scroll.js | 32 - app/javascript/mastodon/scroll.ts | 48 + app/javascript/mastodon/selectors/index.js | 12 +- .../mastodon/service_worker/entry.js | 1 + .../service_worker/web_push_locales.js | 5 +- .../service_worker/web_push_notifications.js | 14 +- .../mastodon/store/configureStore.js | 15 - app/javascript/mastodon/store/index.ts | 45 + .../mastodon/store/middlewares/errors.ts | 21 + .../mastodon/store/middlewares/loading_bar.ts | 45 + .../mastodon/store/middlewares/sounds.ts | 62 + app/javascript/mastodon/stream.js | 48 +- .../mastodon/utils/__tests__/html-test.js | 2 +- .../base64.js => mastodon/utils/base64.ts} | 2 +- app/javascript/mastodon/utils/filters.js | 16 - app/javascript/mastodon/utils/filters.ts | 16 + app/javascript/mastodon/utils/hashtags.ts | 29 + .../mastodon/utils/{icons.js => icons.jsx} | 2 - .../mastodon/utils/notifications.js | 2 +- .../mastodon/utils/{numbers.js => numbers.ts} | 50 +- app/javascript/mastodon/utils/resize_image.js | 189 - app/javascript/mastodon/utils/scrollbar.js | 4 +- app/javascript/mastodon/uuid.js | 3 - app/javascript/mastodon/uuid.ts | 9 + app/javascript/packs/{admin.js => admin.jsx} | 17 +- app/javascript/packs/application.js | 18 +- app/javascript/packs/public-path.js | 2 +- .../packs/{public.js => public.jsx} | 94 +- app/javascript/packs/share.js | 26 - app/javascript/packs/share.jsx | 30 + app/javascript/packs/sign_up.js | 16 + app/javascript/styles/application.scss | 1 + app/javascript/styles/fonts/roboto-mono.scss | 6 +- app/javascript/styles/mailer.scss | 2 +- .../styles/mastodon-light/diff.scss | 98 +- .../styles/mastodon-light/variables.scss | 13 + app/javascript/styles/mastodon/about.scss | 4 +- .../styles/mastodon/accessibility.scss | 5 +- app/javascript/styles/mastodon/accounts.scss | 34 +- app/javascript/styles/mastodon/admin.scss | 130 +- app/javascript/styles/mastodon/basics.scss | 8 +- .../styles/mastodon/components.scss | 1710 ++-- .../styles/mastodon/containers.scss | 12 +- app/javascript/styles/mastodon/dashboard.scss | 6 +- .../styles/mastodon/emoji_picker.scss | 12 +- app/javascript/styles/mastodon/forms.scss | 199 +- app/javascript/styles/mastodon/modal.scss | 8 +- app/javascript/styles/mastodon/polls.scss | 20 +- app/javascript/styles/mastodon/rich_text.scss | 64 + app/javascript/styles/mastodon/rtl.scss | 365 +- app/javascript/styles/mastodon/statuses.scss | 12 +- app/javascript/styles/mastodon/tables.scss | 24 +- app/javascript/styles/mastodon/variables.scss | 63 +- app/javascript/styles/mastodon/widgets.scss | 18 +- app/javascript/types/image.d.ts | 30 + app/javascript/types/resources.ts | 54 + app/javascript/types/util.ts | 1 + app/lib/account_reach_finder.rb | 9 +- app/lib/activity_tracker.rb | 16 +- app/lib/activitypub/activity.rb | 8 +- app/lib/activitypub/activity/announce.rb | 2 +- app/lib/activitypub/activity/create.rb | 54 +- app/lib/activitypub/activity/delete.rb | 8 +- app/lib/activitypub/activity/flag.rb | 8 +- app/lib/activitypub/activity/update.rb | 2 +- app/lib/activitypub/case_transform.rb | 2 +- app/lib/activitypub/dereferencer.rb | 11 +- app/lib/activitypub/forwarder.rb | 14 +- app/lib/activitypub/linked_data_signature.rb | 6 +- .../parser/media_attachment_parser.rb | 4 +- app/lib/activitypub/tag_manager.rb | 6 + app/lib/admin/metrics/dimension.rb | 4 +- .../dimension/instance_accounts_dimension.rb | 19 +- .../dimension/instance_languages_dimension.rb | 25 +- .../metrics/dimension/languages_dimension.rb | 19 +- .../admin/metrics/dimension/query_helper.rb | 13 + .../metrics/dimension/servers_dimension.rb | 24 +- .../dimension/software_versions_dimension.rb | 12 +- .../metrics/dimension/sources_dimension.rb | 20 +- .../dimension/space_usage_dimension.rb | 12 +- .../dimension/tag_languages_dimension.rb | 29 +- .../dimension/tag_servers_dimension.rb | 30 +- app/lib/admin/metrics/measure.rb | 4 +- .../measure/instance_accounts_measure.rb | 24 +- .../measure/instance_followers_measure.rb | 24 +- .../measure/instance_follows_measure.rb | 24 +- .../instance_media_attachments_measure.rb | 23 +- .../measure/instance_reports_measure.rb | 24 +- .../measure/instance_statuses_measure.rb | 30 +- .../metrics/measure/new_users_measure.rb | 16 +- .../metrics/measure/opened_reports_measure.rb | 16 +- app/lib/admin/metrics/measure/query_helper.rb | 25 + .../measure/resolved_reports_measure.rb | 16 +- .../metrics/measure/tag_servers_measure.rb | 24 +- app/lib/admin/metrics/retention.rb | 74 +- app/lib/admin/system_check.rb | 1 + .../admin/system_check/elasticsearch_check.rb | 2 +- .../admin/system_check/media_privacy_check.rb | 105 + app/lib/admin/system_check/message.rb | 11 +- app/lib/advanced_text_formatter.rb | 1 + app/lib/application_extension.rb | 4 - app/lib/attachment_batch.rb | 111 + .../connection_pool/shared_connection_pool.rb | 12 +- app/lib/delivery_failure_tracker.rb | 2 +- app/lib/emoji_formatter.rb | 13 +- app/lib/extractor.rb | 18 +- app/lib/feed_manager.rb | 64 +- app/lib/hash_object.rb | 10 - app/lib/html_aware_formatter.rb | 2 + app/lib/importer/accounts_index_importer.rb | 4 +- app/lib/importer/base_importer.rb | 2 +- app/lib/importer/statuses_index_importer.rb | 12 +- app/lib/importer/tags_index_importer.rb | 4 +- app/lib/link_details_extractor.rb | 54 +- app/lib/ostatus/tag_manager.rb | 32 +- app/lib/permalink_redirector.rb | 48 +- app/lib/plain_text_formatter.rb | 8 +- app/lib/rate_limiter.rb | 2 +- app/lib/request.rb | 39 +- app/lib/request_pool.rb | 7 +- app/lib/scope_parser.rb | 2 +- app/lib/scope_transformer.rb | 2 +- app/lib/settings/extend.rb | 9 - app/lib/settings/scoped_settings.rb | 2 + app/lib/status_filter.rb | 1 + app/lib/status_finder.rb | 4 +- app/lib/tag_manager.rb | 7 +- app/lib/text_formatter.rb | 6 +- app/lib/themes.rb | 45 +- app/lib/toc_generator.rb | 69 - app/lib/translation_service.rb | 4 + app/lib/translation_service/deepl.rb | 69 +- .../translation_service/libre_translate.rb | 51 +- app/lib/user_settings_decorator.rb | 180 - app/lib/user_settings_serializer.rb | 19 + app/lib/vacuum/access_tokens_vacuum.rb | 6 +- app/lib/vacuum/imports_vacuum.rb | 18 + app/lib/vacuum/media_attachments_vacuum.rb | 10 +- app/lib/validation_error_formatter.rb | 2 +- app/lib/webfinger.rb | 5 +- app/lib/webfinger_resource.rb | 4 +- app/lib/webhooks/payload_renderer.rb | 67 + app/mailers/application_mailer.rb | 14 +- app/mailers/notification_mailer.rb | 30 +- app/models/account.rb | 149 +- app/models/account/field.rb | 16 +- app/models/account_conversation.rb | 36 +- app/models/account_domain_block.rb | 1 + app/models/account_filter.rb | 26 +- app/models/account_migration.rb | 2 +- app/models/account_moderation_note.rb | 1 + app/models/account_note.rb | 1 + app/models/account_pin.rb | 1 + app/models/account_stat.rb | 3 +- app/models/account_statuses_cleanup_policy.rb | 36 +- app/models/account_statuses_filter.rb | 6 +- .../account_suggestions/setting_source.rb | 8 +- app/models/account_suggestions/source.rb | 4 +- app/models/account_summary.rb | 1 + app/models/account_warning.rb | 21 +- app/models/admin/account_action.rb | 17 +- app/models/admin/action_log.rb | 2 +- app/models/admin/appeal_filter.rb | 4 +- app/models/admin/import.rb | 1 + app/models/admin/status_filter.rb | 4 +- app/models/announcement.rb | 14 +- app/models/announcement_reaction.rb | 3 +- app/models/appeal.rb | 2 +- app/models/backup.rb | 5 +- app/models/block.rb | 3 +- app/models/bookmark.rb | 1 + app/models/bulk_import.rb | 54 + app/models/bulk_import_row.rb | 15 + app/models/canonical_email_block.rb | 1 + app/models/concerns/account_associations.rb | 3 + app/models/concerns/account_interactions.rb | 44 +- app/models/concerns/account_merging.rb | 16 +- app/models/concerns/account_search.rb | 151 + app/models/concerns/attachmentable.rb | 4 +- app/models/concerns/expireable.rb | 2 +- app/models/concerns/has_user_settings.rb | 173 + app/models/concerns/ldap_authenticable.rb | 2 +- app/models/concerns/lockable.rb | 2 +- app/models/concerns/omniauthable.rb | 11 +- app/models/concerns/paginable.rb | 4 +- app/models/concerns/pam_authenticable.rb | 12 +- app/models/concerns/remotable.rb | 4 +- .../concerns/status_safe_reblog_insert.rb | 72 + .../concerns/status_threading_concern.rb | 14 +- app/models/conversation.rb | 1 + app/models/conversation_mute.rb | 1 + app/models/custom_emoji.rb | 4 +- app/models/custom_filter.rb | 11 +- app/models/custom_filter_keyword.rb | 1 + app/models/custom_filter_status.rb | 1 + app/models/device.rb | 1 + app/models/direct_feed.rb | 9 +- app/models/domain_allow.rb | 2 +- app/models/domain_block.rb | 7 +- app/models/email_domain_block.rb | 15 +- app/models/encrypted_message.rb | 1 + app/models/favourite.rb | 2 + app/models/featured_tag.rb | 1 + app/models/follow.rb | 1 + app/models/follow_recommendation.rb | 5 +- app/models/follow_recommendation_filter.rb | 2 +- .../follow_recommendation_suppression.rb | 5 +- app/models/follow_request.rb | 4 +- app/models/form/account_batch.rb | 27 +- app/models/form/admin_settings.rb | 16 +- app/models/form/custom_emoji_batch.rb | 12 +- app/models/form/import.rb | 156 + app/models/identity.rb | 3 +- app/models/import.rb | 7 +- app/models/instance.rb | 12 +- app/models/invite.rb | 1 + app/models/ip_block.rb | 1 + app/models/list.rb | 4 +- app/models/list_account.rb | 26 +- app/models/login_activity.rb | 1 + app/models/media_attachment.rb | 27 +- app/models/mention.rb | 1 + app/models/mute.rb | 1 + app/models/notification.rb | 41 +- app/models/one_time_key.rb | 1 + app/models/poll.rb | 10 +- app/models/poll_vote.rb | 2 + app/models/preview_card.rb | 9 +- app/models/preview_card_provider.rb | 2 + app/models/relationship_filter.rb | 4 +- app/models/relay.rb | 3 +- app/models/remote_follow.rb | 12 +- app/models/report.rb | 16 +- app/models/report_note.rb | 1 + app/models/session_activation.rb | 6 +- app/models/setting.rb | 7 +- app/models/site_upload.rb | 3 +- app/models/status.rb | 165 +- app/models/status_edit.rb | 18 +- app/models/status_pin.rb | 1 + app/models/status_stat.rb | 1 + app/models/system_key.rb | 2 +- app/models/tag.rb | 9 +- app/models/translation.rb | 14 + app/models/trends/history.rb | 6 +- app/models/trends/links.rb | 24 +- app/models/trends/preview_card_filter.rb | 4 +- app/models/trends/status_filter.rb | 4 +- app/models/trends/statuses.rb | 24 +- app/models/trends/tag_filter.rb | 12 +- app/models/trends/tags.rb | 12 +- app/models/unavailable_domain.rb | 1 + app/models/user.rb | 106 +- app/models/user_ip.rb | 3 +- app/models/user_role.rb | 4 +- app/models/user_settings.rb | 109 + app/models/user_settings/dsl.rb | 37 + app/models/user_settings/glue.rb | 23 + app/models/user_settings/namespace.rb | 21 + app/models/user_settings/setting.rb | 49 + app/models/web/push_subscription.rb | 25 +- app/models/web/setting.rb | 1 + app/models/webauthn_credential.rb | 3 +- app/models/webhook.rb | 42 +- app/policies/webhook_policy.rb | 4 +- .../account_relationships_presenter.rb | 18 +- app/presenters/instance_presenter.rb | 12 +- .../status_relationships_presenter.rb | 4 +- app/presenters/tag_relationships_presenter.rb | 12 +- .../activitypub/note_serializer.rb | 1 + app/serializers/initial_state_serializer.rb | 8 +- app/serializers/rest/account_serializer.rb | 28 +- .../rest/admin/account_serializer.rb | 3 +- .../rest/admin/trends/link_serializer.rb | 9 + .../links/preview_card_provider_serializer.rb | 10 + .../rest/admin/trends/status_serializer.rb | 9 + .../rest/admin/webhook_event_serializer.rb | 2 + app/serializers/rest/instance_serializer.rb | 9 +- app/serializers/rest/list_serializer.rb | 2 +- app/serializers/rest/mute_serializer.rb | 4 +- .../rest/preview_card_serializer.rb | 2 +- .../rest/privacy_policy_serializer.rb | 2 +- app/serializers/rest/status_serializer.rb | 4 +- .../rest/translation_serializer.rb | 35 +- .../rest/v1/instance_serializer.rb | 1 + .../rest/web_push_subscription_serializer.rb | 6 +- app/services/account_search_service.rb | 67 +- .../fetch_featured_collection_service.rb | 6 +- .../fetch_featured_tags_collection_service.rb | 16 +- .../fetch_remote_account_service.rb | 2 +- .../activitypub/fetch_remote_actor_service.rb | 11 +- .../activitypub/fetch_remote_key_service.rb | 2 +- .../fetch_remote_status_service.rb | 29 +- .../activitypub/fetch_replies_service.rb | 9 +- ...epare_followers_synchronization_service.rb | 2 +- .../activitypub/process_account_service.rb | 4 +- .../activitypub/process_collection_service.rb | 6 +- .../process_status_update_service.rb | 10 +- .../synchronize_followers_service.rb | 2 +- app/services/app_sign_up_service.rb | 2 +- app/services/backup_service.rb | 156 +- app/services/batched_remove_status_service.rb | 18 +- app/services/bulk_import_row_service.rb | 66 + app/services/bulk_import_service.rb | 182 + app/services/clear_domain_media_service.rb | 30 +- app/services/delete_account_service.rb | 6 +- app/services/fan_out_on_write_service.rb | 2 +- app/services/favourite_service.rb | 1 + app/services/fetch_link_card_service.rb | 24 +- app/services/fetch_oembed_service.rb | 4 +- app/services/fetch_remote_status_service.rb | 4 +- app/services/fetch_resource_service.rb | 7 +- app/services/follow_migration_service.rb | 62 + app/services/import_service.rb | 21 +- app/services/keys/claim_service.rb | 8 +- app/services/keys/query_service.rb | 8 +- app/services/notify_service.rb | 16 +- app/services/post_status_service.rb | 58 +- app/services/process_mentions_service.rb | 33 +- app/services/reblog_service.rb | 12 +- .../remove_domains_from_followers_service.rb | 23 + app/services/remove_from_followers_service.rb | 4 +- app/services/remove_status_service.rb | 22 +- app/services/resolve_account_service.rb | 22 +- app/services/resolve_url_service.rb | 23 +- app/services/search_service.rb | 35 +- app/services/suspend_account_service.rb | 14 +- app/services/tag_search_service.rb | 2 +- app/services/translate_status_service.rb | 97 +- app/services/unfollow_service.rb | 2 +- app/services/unsuspend_account_service.rb | 4 +- app/services/update_account_service.rb | 2 +- app/services/update_status_service.rb | 4 +- app/services/verify_link_service.rb | 6 +- app/services/vote_service.rb | 4 +- app/validators/domain_validator.rb | 12 +- app/validators/ed25519_key_validator.rb | 2 +- app/validators/ed25519_signature_validator.rb | 2 +- app/validators/email_mx_validator.rb | 2 +- app/validators/existing_username_validator.rb | 12 +- app/validators/follow_limit_validator.rb | 1 + app/validators/html_validator.rb | 20 - app/validators/import_validator.rb | 48 - app/validators/status_pin_validator.rb | 2 +- .../unreserved_username_validator.rb | 2 + app/validators/vote_validator.rb | 46 +- app/views/accounts/show.rss.ruby | 12 +- app/views/admin/account_actions/new.html.haml | 2 +- app/views/admin/accounts/show.html.haml | 12 +- app/views/admin/action_logs/index.html.haml | 4 +- app/views/admin/announcements/index.html.haml | 2 +- app/views/admin/change_emails/show.html.haml | 2 +- app/views/admin/custom_emojis/index.html.haml | 4 +- app/views/admin/dashboard/index.html.haml | 4 +- .../admin/disputes/appeals/index.html.haml | 2 +- .../confirm_suspension.html.haml | 22 + .../_email_domain_block.html.haml | 2 +- .../_domain_block.html.haml | 6 +- app/views/admin/instances/_instance.html.haml | 2 +- app/views/admin/instances/index.html.haml | 2 +- app/views/admin/instances/show.html.haml | 2 +- app/views/admin/ip_blocks/_ip_block.html.haml | 2 +- .../reports/_media_attachments.html.haml | 6 +- .../admin/reports/actions/preview.html.haml | 14 +- app/views/admin/roles/_role.html.haml | 2 +- app/views/admin/rules/index.html.haml | 2 +- app/views/admin/settings/about/show.html.haml | 3 + .../settings/content_retention/show.html.haml | 2 +- .../settings/registrations/show.html.haml | 2 +- app/views/admin/statuses/show.html.haml | 2 +- .../trends/links/_preview_card.html.haml | 10 +- .../admin/trends/statuses/_status.html.haml | 10 +- app/views/admin/trends/tags/_tag.html.haml | 6 +- .../admin/warning_presets/index.html.haml | 2 +- app/views/admin/webhooks/_form.html.haml | 5 +- app/views/admin/webhooks/_webhook.html.haml | 2 +- app/views/admin/webhooks/index.html.haml | 2 +- app/views/admin/webhooks/show.html.haml | 16 +- .../admin_mailer/_new_trending_links.text.erb | 4 +- .../_new_trending_statuses.text.erb | 2 +- .../admin_mailer/_new_trending_tags.text.erb | 2 +- app/views/application/_card.html.haml | 8 +- app/views/application/_sidebar.html.haml | 16 - .../auth/confirmations/captcha.html.haml | 14 +- app/views/auth/confirmations/new.html.haml | 2 +- .../auth/registrations/_sessions.html.haml | 2 +- app/views/auth/registrations/edit.html.haml | 4 +- app/views/auth/registrations/new.html.haml | 11 +- app/views/auth/registrations/rules.html.haml | 14 +- app/views/auth/sessions/new.html.haml | 2 +- app/views/auth/setup/show.html.haml | 26 +- app/views/auth/shared/_links.html.haml | 2 +- app/views/auth/shared/_progress.html.haml | 25 + app/views/disputes/strikes/show.html.haml | 14 +- app/views/filters/_filter_fields.html.haml | 2 +- app/views/filters/index.html.haml | 2 +- app/views/kaminari/_gap.html.haml | 9 + app/views/kaminari/_next_page.html.haml | 16 +- app/views/kaminari/_paginator.html.haml | 15 +- app/views/kaminari/_prev_page.html.haml | 15 +- app/views/layouts/_theme.html.haml | 5 +- app/views/layouts/application.html.haml | 10 +- app/views/layouts/embedded.html.haml | 6 +- app/views/layouts/error.html.haml | 2 +- app/views/layouts/mailer.html.haml | 8 +- app/views/layouts/modal.html.haml | 2 +- app/views/mail_subscriptions/create.html.haml | 9 + app/views/mail_subscriptions/show.html.haml | 12 + app/views/media/player.html.haml | 6 +- .../notification_mailer/_status.html.haml | 8 +- .../notification_mailer/favourite.html.haml | 4 +- .../follow_request.html.haml | 2 +- .../notification_mailer/mention.html.haml | 2 +- .../notification_mailer/reblog.html.haml | 2 +- app/views/oauth/authorizations/show.html.haml | 2 +- .../authorized_applications/index.html.haml | 6 +- app/views/relationships/show.html.haml | 2 +- .../settings/applications/index.html.haml | 2 +- .../settings/applications/show.html.haml | 4 +- app/views/settings/exports/show.html.haml | 2 +- .../settings/featured_tags/index.html.haml | 10 +- app/views/settings/flavours/show.html.haml | 4 +- app/views/settings/imports/index.html.haml | 49 + app/views/settings/imports/show.html.haml | 20 +- .../_login_activity.html.haml | 2 +- .../settings/login_activities/index.html.haml | 2 +- .../preferences/appearance/show.html.haml | 76 +- .../preferences/notifications/show.html.haml | 38 +- .../settings/preferences/other/show.html.haml | 41 +- app/views/settings/profiles/show.html.haml | 102 +- app/views/settings/shared/_links.html.haml | 10 - .../shared/_profile_navigation.html.haml | 6 + .../settings/verifications/show.html.haml | 30 + app/views/shared/_og.html.haml | 2 +- app/views/statuses/_detailed_status.html.haml | 6 +- app/views/statuses/_og_image.html.haml | 5 +- app/views/statuses/_poll.html.haml | 2 +- app/views/statuses/_status.html.haml | 2 +- app/views/tags/show.rss.ruby | 12 +- .../user_mailer/appeal_approved.html.haml | 2 +- .../user_mailer/appeal_approved.text.erb | 2 +- .../user_mailer/appeal_rejected.html.haml | 4 +- .../user_mailer/appeal_rejected.text.erb | 2 +- app/views/user_mailer/backup_ready.html.haml | 2 +- app/views/user_mailer/backup_ready.text.erb | 2 +- .../user_mailer/suspicious_sign_in.html.haml | 4 +- .../user_mailer/suspicious_sign_in.text.erb | 2 +- app/views/user_mailer/warning.html.haml | 2 +- .../webauthn_credential_added.html.haml | 2 +- .../webauthn_credential_deleted.html.haml | 2 +- app/views/well_known/host_meta/show.xml.ruby | 2 +- app/workers/account_deletion_worker.rb | 5 +- app/workers/activitypub/delivery_worker.rb | 10 + .../distribute_poll_update_worker.rb | 2 +- .../activitypub/fetch_replies_worker.rb | 4 +- .../migrated_follow_delivery_worker.rb | 17 + .../activitypub/move_distribution_worker.rb | 2 +- app/workers/activitypub/processing_worker.rb | 2 +- .../activitypub/raw_distribution_worker.rb | 2 +- app/workers/backup_worker.rb | 10 +- app/workers/bulk_import_worker.rb | 13 + app/workers/concerns/exponential_backoff.rb | 2 +- app/workers/distribution_worker.rb | 2 +- app/workers/fetch_reply_worker.rb | 4 +- app/workers/import/relationship_worker.rb | 5 +- app/workers/import/row_worker.rb | 33 + app/workers/import_worker.rb | 3 + app/workers/move_worker.rb | 42 +- app/workers/poll_expiration_notify_worker.rb | 2 +- app/workers/post_process_media_worker.rb | 14 +- .../accounts_statuses_cleanup_scheduler.rb | 110 +- .../follow_recommendations_scheduler.rb | 23 +- app/workers/scheduler/indexing_scheduler.rb | 14 +- .../scheduler/user_cleanup_scheduler.rb | 4 +- app/workers/scheduler/vacuum_scheduler.rb | 5 + app/workers/thread_resolve_worker.rb | 4 +- app/workers/unfollow_follow_worker.rb | 8 +- app/workers/web/push_notification_worker.rb | 12 +- app/workers/webhooks/delivery_worker.rb | 4 +- babel.config.js | 12 +- bin/tootctl | 6 +- config.ru | 3 +- config/application.rb | 21 + config/boot.rb | 10 +- config/database.yml | 1 + config/deploy.rb | 9 +- config/environments/development.rb | 33 +- config/environments/production.rb | 15 +- config/environments/test.rb | 15 +- config/formatjs-formatter.js | 14 + config/i18n-tasks.yml | 3 + config/initializers/0_duplicate_migrations.rb | 24 +- config/initializers/chewy.rb | 2 +- config/initializers/ffmpeg.rb | 2 +- config/initializers/inflections.rb | 1 + config/initializers/omniauth.rb | 2 +- config/initializers/paperclip.rb | 19 +- config/initializers/rack_attack.rb | 18 +- config/initializers/redis.rb | 1 + config/initializers/simple_form.rb | 11 + config/initializers/statsd.rb | 15 - config/initializers/strong_migrations.rb | 3 +- config/initializers/twitter_regex.rb | 16 +- config/initializers/webauthn.rb | 2 +- config/locales-glitch/de.yml | 4 +- config/locales-glitch/en.yml | 2 +- config/locales-glitch/es-AR.yml | 22 + config/locales-glitch/es-MX.yml | 22 + config/locales-glitch/es.yml | 32 +- config/locales-glitch/no.yml | 3 +- config/locales-glitch/pl.yml | 36 + config/locales-glitch/pt-PT.yml | 41 + config/locales-glitch/simple_form.en.yml | 1 - config/locales-glitch/simple_form.es-AR.yml | 9 + config/locales-glitch/simple_form.es-MX.yml | 9 + config/locales-glitch/simple_form.es.yml | 9 + config/locales-glitch/simple_form.no.yml | 3 +- config/locales-glitch/simple_form.pl.yml | 17 + config/locales-glitch/simple_form.zh-CN.yml | 15 +- config/locales-glitch/zh-CN.yml | 24 +- config/locales/activerecord.bg.yml | 4 +- config/locales/activerecord.ca.yml | 2 +- config/locales/activerecord.cs.yml | 2 +- config/locales/activerecord.de.yml | 16 +- config/locales/activerecord.el.yml | 13 + config/locales/activerecord.en.yml | 4 + config/locales/activerecord.eo.yml | 2 +- config/locales/activerecord.fi.yml | 4 +- config/locales/activerecord.fr.yml | 2 +- config/locales/activerecord.fy.yml | 10 +- config/locales/activerecord.hu.yml | 2 +- config/locales/activerecord.ka.yml | 37 + config/locales/activerecord.kab.yml | 4 + config/locales/activerecord.my.yml | 54 + config/locales/activerecord.pt-BR.yml | 2 +- config/locales/activerecord.pt-PT.yml | 8 +- config/locales/activerecord.tr.yml | 2 +- config/locales/activerecord.tt.yml | 4 +- config/locales/activerecord.uz.yml | 55 + config/locales/activerecord.zh-TW.yml | 2 +- config/locales/activerecord.zh_Hant.yml | 15 - config/locales/af.yml | 8 - config/locales/an.yml | 39 +- config/locales/ar.yml | 132 +- config/locales/ast.yml | 163 +- config/locales/be.yml | 86 +- config/locales/bg.yml | 142 +- config/locales/bn.yml | 12 - config/locales/br.yml | 15 - config/locales/bs.yml | 10 - config/locales/ca.yml | 166 +- config/locales/ckb.yml | 22 - config/locales/co.yml | 21 - config/locales/cs.yml | 90 +- config/locales/cy.yml | 118 +- config/locales/da.yml | 108 +- config/locales/de.yml | 704 +- config/locales/devise.ar.yml | 4 +- config/locales/devise.ast.yml | 4 +- config/locales/devise.bg.yml | 14 +- config/locales/devise.de.yml | 60 +- config/locales/devise.en.yml | 6 +- config/locales/devise.eo.yml | 2 +- config/locales/devise.et.yml | 10 +- config/locales/devise.fa.yml | 36 +- config/locales/devise.fi.yml | 46 +- config/locales/devise.fr.yml | 22 +- config/locales/devise.fy.yml | 6 +- config/locales/devise.ga.yml | 6 + config/locales/devise.hu.yml | 98 +- config/locales/devise.ko.yml | 28 +- config/locales/devise.my.yml | 113 + config/locales/devise.nl.yml | 2 +- config/locales/devise.pt-PT.yml | 44 +- config/locales/devise.th.yml | 14 +- config/locales/devise.uz.yml | 13 + config/locales/devise.zh-CN.yml | 6 +- config/locales/devise.zh-TW.yml | 14 +- config/locales/doorkeeper.af.yml | 2 - config/locales/doorkeeper.an.yml | 14 +- config/locales/doorkeeper.ar.yml | 4 +- config/locales/doorkeeper.ast.yml | 24 +- config/locales/doorkeeper.be.yml | 4 +- config/locales/doorkeeper.bg.yml | 6 +- config/locales/doorkeeper.ca.yml | 4 +- config/locales/doorkeeper.ckb.yml | 2 - config/locales/doorkeeper.cs.yml | 4 +- config/locales/doorkeeper.cy.yml | 20 +- config/locales/doorkeeper.da.yml | 4 +- config/locales/doorkeeper.de.yml | 42 +- config/locales/doorkeeper.el.yml | 21 +- config/locales/doorkeeper.en-GB.yml | 4 +- config/locales/doorkeeper.eo.yml | 18 +- config/locales/doorkeeper.es-AR.yml | 4 +- config/locales/doorkeeper.es-MX.yml | 4 +- config/locales/doorkeeper.es.yml | 4 +- config/locales/doorkeeper.et.yml | 6 +- config/locales/doorkeeper.eu.yml | 14 +- config/locales/doorkeeper.fa.yml | 4 +- config/locales/doorkeeper.fi.yml | 26 +- config/locales/doorkeeper.fo.yml | 4 +- config/locales/doorkeeper.fr-QC.yml | 14 +- config/locales/doorkeeper.fr.yml | 16 +- config/locales/doorkeeper.fy.yml | 4 +- config/locales/doorkeeper.ga.yml | 10 +- config/locales/doorkeeper.gd.yml | 14 +- config/locales/doorkeeper.gl.yml | 20 +- config/locales/doorkeeper.he.yml | 4 +- config/locales/doorkeeper.hu.yml | 72 +- config/locales/doorkeeper.hy.yml | 3 +- config/locales/doorkeeper.id.yml | 2 - config/locales/doorkeeper.io.yml | 2 - config/locales/doorkeeper.is.yml | 4 +- config/locales/doorkeeper.it.yml | 4 +- config/locales/doorkeeper.ja.yml | 14 +- config/locales/doorkeeper.ko.yml | 60 +- config/locales/doorkeeper.ku.yml | 14 +- config/locales/doorkeeper.lv.yml | 4 +- config/locales/doorkeeper.ms.yml | 2 - config/locales/doorkeeper.my.yml | 194 + config/locales/doorkeeper.nl.yml | 4 +- config/locales/doorkeeper.nn.yml | 4 +- config/locales/doorkeeper.no.yml | 4 +- config/locales/doorkeeper.oc.yml | 9 +- config/locales/doorkeeper.pl.yml | 6 +- config/locales/doorkeeper.pt-BR.yml | 14 +- config/locales/doorkeeper.pt-PT.yml | 28 +- config/locales/doorkeeper.ro.yml | 2 - config/locales/doorkeeper.ru.yml | 12 +- config/locales/doorkeeper.sco.yml | 2 - config/locales/doorkeeper.si.yml | 2 - config/locales/doorkeeper.sk.yml | 3 +- config/locales/doorkeeper.sl.yml | 4 +- config/locales/doorkeeper.sq.yml | 14 +- config/locales/doorkeeper.sr-Latn.yml | 19 +- config/locales/doorkeeper.sr.yml | 19 +- config/locales/doorkeeper.sv.yml | 4 +- config/locales/doorkeeper.th.yml | 8 +- config/locales/doorkeeper.tr.yml | 18 +- config/locales/doorkeeper.uk.yml | 4 +- config/locales/doorkeeper.uz.yml | 1 + config/locales/doorkeeper.vi.yml | 4 +- config/locales/doorkeeper.zh-CN.yml | 4 +- config/locales/doorkeeper.zh-HK.yml | 4 +- config/locales/doorkeeper.zh-TW.yml | 12 +- config/locales/el.yml | 1111 +- config/locales/en-GB.yml | 1634 ++- config/locales/en.yml | 178 +- config/locales/en_GB.yml | 1043 -- config/locales/eo.yml | 192 +- config/locales/es-AR.yml | 116 +- config/locales/es-MX.yml | 88 +- config/locales/es.yml | 90 +- config/locales/et.yml | 126 +- config/locales/eu.yml | 116 +- config/locales/fa.yml | 236 +- config/locales/fi.yml | 276 +- config/locales/fo.yml | 88 +- config/locales/fr-QC.yml | 92 +- config/locales/fr.yml | 146 +- config/locales/fy.yml | 88 +- config/locales/ga.yml | 216 +- config/locales/gd.yml | 85 +- config/locales/gl.yml | 96 +- config/locales/he.yml | 96 +- config/locales/hi.yml | 26 +- config/locales/hr.yml | 16 - config/locales/hu.yml | 116 +- config/locales/hy.yml | 38 +- config/locales/id.yml | 25 +- config/locales/ig.yml | 10 - config/locales/io.yml | 23 - config/locales/is.yml | 88 +- config/locales/it.yml | 94 +- config/locales/ja.yml | 101 +- config/locales/ka.yml | 14 - config/locales/kab.yml | 13 +- config/locales/kk.yml | 20 - config/locales/kn.yml | 10 - config/locales/ko.yml | 191 +- config/locales/ku.yml | 23 - config/locales/kw.yml | 10 - config/locales/la.yml | 2 - config/locales/lt.yml | 18 - config/locales/lv.yml | 106 +- config/locales/mk.yml | 10 - config/locales/ml.yml | 13 - config/locales/mr.yml | 10 - config/locales/ms.yml | 6 - config/locales/my.yml | 1703 +++- config/locales/nl.yml | 102 +- config/locales/nn.yml | 134 +- config/locales/no.yml | 64 +- config/locales/oc.yml | 29 +- config/locales/pa.yml | 2 - config/locales/pl.yml | 130 +- config/locales/pt-BR.yml | 98 +- config/locales/pt-PT.yml | 138 +- config/locales/ro.yml | 16 - config/locales/ru.yml | 91 +- config/locales/sa.yml | 10 - config/locales/sc.yml | 20 - config/locales/sco.yml | 23 - config/locales/si.yml | 23 - config/locales/simple_form.an.yml | 10 +- config/locales/simple_form.ar.yml | 17 +- config/locales/simple_form.ast.yml | 50 +- config/locales/simple_form.be.yml | 15 +- config/locales/simple_form.bg.yml | 121 +- config/locales/simple_form.ca.yml | 19 +- config/locales/simple_form.ckb.yml | 2 - config/locales/simple_form.co.yml | 2 - config/locales/simple_form.cs.yml | 15 +- config/locales/simple_form.cy.yml | 19 +- config/locales/simple_form.da.yml | 17 +- config/locales/simple_form.de.yml | 109 +- config/locales/simple_form.el.yml | 125 +- config/locales/simple_form.en-GB.yml | 187 +- config/locales/simple_form.en.yml | 22 +- config/locales/simple_form.en_GB.yml | 131 - config/locales/simple_form.eo.yml | 28 +- config/locales/simple_form.es-AR.yml | 14 +- config/locales/simple_form.es-MX.yml | 15 +- config/locales/simple_form.es.yml | 15 +- config/locales/simple_form.et.yml | 23 +- config/locales/simple_form.eu.yml | 15 +- config/locales/simple_form.fa.yml | 86 +- config/locales/simple_form.fi.yml | 29 +- config/locales/simple_form.fo.yml | 15 +- config/locales/simple_form.fr-QC.yml | 14 +- config/locales/simple_form.fr.yml | 64 +- config/locales/simple_form.fy.yml | 15 +- config/locales/simple_form.ga.yml | 28 +- config/locales/simple_form.gd.yml | 16 +- config/locales/simple_form.gl.yml | 15 +- config/locales/simple_form.he.yml | 17 +- config/locales/simple_form.hu.yml | 15 +- config/locales/simple_form.hy.yml | 2 - config/locales/simple_form.id.yml | 4 - config/locales/simple_form.io.yml | 4 - config/locales/simple_form.is.yml | 15 +- config/locales/simple_form.it.yml | 15 +- config/locales/simple_form.ja.yml | 49 +- config/locales/simple_form.ka.yml | 1 - config/locales/simple_form.kab.yml | 1 - config/locales/simple_form.ko.yml | 59 +- config/locales/simple_form.ku.yml | 4 - config/locales/simple_form.lv.yml | 15 +- config/locales/simple_form.ms.yml | 2 - config/locales/simple_form.my.yml | 315 + config/locales/simple_form.nl.yml | 19 +- config/locales/simple_form.nn.yml | 4 - config/locales/simple_form.no.yml | 5 +- config/locales/simple_form.oc.yml | 16 +- config/locales/simple_form.pl.yml | 29 +- config/locales/simple_form.pt-BR.yml | 15 +- config/locales/simple_form.pt-PT.yml | 177 +- config/locales/simple_form.ro.yml | 2 - config/locales/simple_form.ru.yml | 17 +- config/locales/simple_form.sc.yml | 4 - config/locales/simple_form.sco.yml | 4 - config/locales/simple_form.si.yml | 4 - config/locales/simple_form.sk.yml | 9 +- config/locales/simple_form.sl.yml | 15 +- config/locales/simple_form.sq.yml | 21 +- config/locales/simple_form.sr-Latn.yml | 144 +- config/locales/simple_form.sr.yml | 143 +- config/locales/simple_form.sv.yml | 15 +- config/locales/simple_form.th.yml | 35 +- config/locales/simple_form.tr.yml | 19 +- config/locales/simple_form.tt.yml | 4 +- config/locales/simple_form.uk.yml | 14 +- config/locales/simple_form.uz.yml | 1 + config/locales/simple_form.vi.yml | 13 +- config/locales/simple_form.zh-CN.yml | 17 +- config/locales/simple_form.zh-HK.yml | 14 +- config/locales/simple_form.zh-TW.yml | 45 +- config/locales/sk.yml | 178 +- config/locales/sl.yml | 88 +- config/locales/sq.yml | 112 +- config/locales/sr-Latn.yml | 1545 ++- config/locales/sr.yml | 1164 ++- config/locales/sv.yml | 83 +- config/locales/szl.yml | 10 - config/locales/ta.yml | 10 - config/locales/tai.yml | 12 - config/locales/te.yml | 10 - config/locales/th.yml | 138 +- config/locales/tr.yml | 118 +- config/locales/tt.yml | 51 +- config/locales/ug.yml | 10 - config/locales/uk.yml | 86 +- config/locales/ur.yml | 10 - config/locales/uz.yml | 41 + config/locales/vi.yml | 82 +- config/locales/zgh.yml | 11 - config/locales/zh-CN.yml | 130 +- config/locales/zh-HK.yml | 78 +- config/locales/zh-TW.yml | 464 +- config/navigation.rb | 8 +- config/puma.rb | 2 + config/routes.rb | 569 +- config/routes/admin.rb | 205 + config/routes/api.rb | 308 + config/routes/settings.rb | 75 + config/settings.yml | 42 +- config/sidekiq.yml | 98 +- config/webpack/configuration.js | 5 +- config/webpack/development.js | 3 +- config/webpack/generateLocalePacks.js | 74 - config/webpack/production.js | 8 +- config/webpack/rules/babel.js | 3 +- config/webpack/rules/file.js | 1 + config/webpack/rules/index.js | 2 +- config/webpack/rules/node_modules.js | 1 + config/webpack/shared.js | 19 +- config/webpack/tests.js | 1 + config/webpack/translationRunner.js | 122 - config/webpacker.yml | 5 +- crowdin.yml | 1 + .../20160305115639_add_devise_to_users.rb | 2 +- ...20161006213403_rails_settings_migration.rb | 10 +- .../20161122163057_remove_unneeded_indexes.rb | 6 +- ...0125145934_add_spoiler_text_to_statuses.rb | 2 +- ...000_add_statuses_index_on_account_id_id.rb | 2 +- ...16191202_add_hide_notifications_to_mute.rb | 14 +- ..._existing_mutes_to_hiding_notifications.rb | 11 +- db/migrate/20170918125918_ids_to_bigints.rb | 26 +- ...70920024819_status_ids_to_timestamp_ids.rb | 4 +- .../20170920032311_fix_reblogs_in_feeds.rb | 2 +- ...09_add_description_to_media_attachments.rb | 2 +- ...170928082043_create_email_domain_blocks.rb | 2 +- ...5102658_create_account_moderation_notes.rb | 2 +- ...005171936_add_disabled_to_custom_emojis.rb | 2 +- ...20171006142024_add_uri_to_custom_emojis.rb | 2 +- .../20171009222537_create_keyword_mutes.rb | 2 + ...foreign_key_to_account_moderation_notes.rb | 2 +- ...nonnullable_in_account_moderation_notes.rb | 2 +- ...8_add_visible_in_picker_to_custom_emoji.rb | 6 +- ...ove_keyword_mutes_into_glitch_namespace.rb | 2 + .../20171028221157_add_reblogs_to_follows.rb | 2 +- ...20171107143332_add_memorial_to_accounts.rb | 2 +- .../20171107143624_add_disabled_to_users.rb | 2 +- ...0171109012327_add_moderator_to_accounts.rb | 2 +- ...add_index_domain_to_email_domain_blocks.rb | 2 +- db/migrate/20171114231651_create_lists.rb | 2 +- .../20171116161857_create_list_accounts.rb | 2 +- ...443_add_moved_to_account_id_to_accounts.rb | 2 +- ...20171119172437_create_admin_action_logs.rb | 2 +- ...ex_account_and_reblog_of_id_to_statuses.rb | 2 +- db/migrate/20171125024930_create_invites.rb | 2 +- .../20171125031751_add_invite_id_to_users.rb | 2 +- ...ex_reblog_of_id_and_account_to_statuses.rb | 2 +- ...735_remove_old_reblog_index_on_statuses.rb | 2 +- ...71129172043_add_index_on_stream_entries.rb | 2 +- ...30000000_add_embed_url_to_preview_cards.rb | 2 +- ..._change_account_id_nonnullable_in_lists.rb | 2 +- ...0213213_add_local_only_flag_to_statuses.rb | 2 + ...95226_remove_duplicate_indexes_in_lists.rb | 6 +- ...4803_more_faster_index_on_notifications.rb | 2 +- ...for_api_v1_accounts_account_id_statuses.rb | 2 +- ...80109143959_add_remember_token_to_users.rb | 2 +- .../20180204034416_create_identities.rb | 6 +- ...180206000000_change_user_id_nonnullable.rb | 2 +- db/migrate/20180211015820_create_backups.rb | 2 +- ...add_featured_collection_url_to_accounts.rb | 2 +- ...ge_columns_in_notifications_nonnullable.rb | 2 +- ...1200_add_assigned_account_id_to_reports.rb | 2 +- .../20180402040909_create_report_notes.rb | 2 +- .../20180410204633_add_fields_to_accounts.rb | 2 +- db/migrate/20180410220657_create_bookmarks.rb | 2 + ...for_api_v1_accounts_account_id_statuses.rb | 10 +- ...for_api_v1_accounts_account_id_statuses.rb | 6 +- ...0180528141303_fix_accounts_unique_index.rb | 41 +- ...apply_to_mentions_flag_to_keyword_mutes.rb | 2 + ...08213548_reject_following_blocked_users.rb | 4 +- .../20180617162849_remove_unused_indexes.rb | 6 +- db/migrate/20180707193142_migrate_filters.rb | 12 +- .../20180812173710_copy_status_stats.rb | 12 +- db/migrate/20180831171112_create_bookmarks.rb | 2 +- ...024224956_migrate_account_conversations.rb | 21 +- .../20181116173541_copy_account_stats.rb | 12 +- ...0190306145741_add_lock_version_to_polls.rb | 1 - ...1829_migrate_open_registrations_setting.rb | 2 + ...0512200918_add_content_type_to_statuses.rb | 2 + ..._preserve_old_layout_for_existing_users.rb | 4 +- ...807135426_add_comments_to_domain_blocks.rb | 1 - ...205_change_list_account_follow_nullable.rb | 2 +- ...2162302_add_status_ids_to_announcements.rb | 1 - ...00407202420_migrate_unavailable_inboxes.rb | 9 +- .../20200510110808_reset_web_app_secret.rb | 3 +- ...181721_remove_duplicated_indexes_pghero.rb | 1 - ..._encrypted_message_ids_to_timestamp_ids.rb | 2 +- ...3_add_fixed_lowercase_index_to_accounts.rb | 2 +- ...5_media_attachment_ids_to_timestamp_ids.rb | 4 +- .../20200628133322_create_account_notes.rb | 1 - .../20200917192924_add_notify_to_follows.rb | 2 +- ...0306164523_account_ids_to_timestamp_ids.rb | 4 +- ...dd_case_insensitive_btree_index_to_tags.rb | 3 +- ...reate_account_statuses_cleanup_policies.rb | 1 - ...175231_add_content_type_to_status_edits.rb | 2 + ...0613110834_add_action_to_custom_filters.rb | 1 + ...20230129023109_add_template_to_webhooks.rb | 7 + .../20230215074327_add_settings_to_users.rb | 7 + .../20230215074423_move_user_settings.rb | 89 + ...0230215074424_move_glitch_user_settings.rb | 62 + .../20230330135507_create_bulk_imports.rb | 22 + .../20230330140036_create_bulk_import_rows.rb | 12 + ..._add_follow_request_id_to_list_accounts.rb | 10 + ...515_add_index_accounts_on_domain_and_id.rb | 9 + ...0230524192812_fix_account_domain_casing.rb | 13 + ...5_add_index_instances_on_reverse_domain.rb | 9 + ...primary_key_to_accounts_tags_join_table.rb | 24 + ...primary_key_to_statuses_tags_join_table.rb | 24 + .../20230605085710_add_exclusive_to_lists.rb | 17 + .../20230605085711_add_time_zone_to_users.rb | 7 + ...0630145300_add_index_backups_on_user_id.rb | 9 + ...1023_add_superapp_index_to_applications.rb | 9 + ...753_add_index_user_on_unconfirmed_email.rb | 9 + .../20180813160548_post_migrate_filters.rb | 6 +- ...emove_suspended_silenced_account_fields.rb | 1 + ...9130537_remove_boosts_widening_audience.rb | 2 +- ...200917193528_migrate_notifications_type.rb | 10 +- ...e_subscription_expires_at_from_accounts.rb | 4 +- ...2_remove_whole_word_from_custom_filters.rb | 1 + ...remove_irreversible_from_custom_filters.rb | 1 + ...71123_fix_custom_filter_keywords_id_seq.rb | 2 +- ...221101190723_backfill_admin_action_logs.rb | 15 +- ...114142_backfill_admin_action_logs_again.rb | 15 +- db/schema.rb | 47 +- db/seeds.rb | 6 +- dist/mastodon-streaming.service | 2 +- dist/nginx.conf | 2 +- docker-compose.yml | 6 +- jest.config.js | 31 +- .../conditional_get_extensions.rb | 15 + .../database_tasks_extensions.rb | 2 +- lib/assets/wordmark.light.css | 4 +- lib/chewy/strategy/bypass_with_warning.rb | 12 + .../post_deployment_migration/USAGE | 10 + .../post_deployment_migration_generator.rb | 17 + .../templates/migration.erb | 8 + .../post_deployment_migration_generator.rb | 17 - lib/linter/haml_middle_dot.rb | 26 + lib/linter/rubocop_middle_dot.rb | 31 + lib/mastodon/cache_cli.rb | 60 - .../{accounts_cli.rb => cli/accounts.rb} | 152 +- lib/mastodon/cli/base.rb | 42 + lib/mastodon/cli/cache.rb | 72 + .../canonical_email_blocks.rb} | 14 +- .../{domains_cli.rb => cli/domains.rb} | 35 +- .../email_domain_blocks.rb} | 18 +- lib/mastodon/{emoji_cli.rb => cli/emoji.rb} | 17 +- lib/mastodon/{feeds_cli.rb => cli/feeds.rb} | 37 +- .../{ip_blocks_cli.rb => cli/ip_blocks.rb} | 47 +- lib/{cli.rb => mastodon/cli/main.rb} | 85 +- .../maintenance.rb} | 299 +- lib/mastodon/{media_cli.rb => cli/media.rb} | 68 +- .../preview_cards.rb} | 18 +- .../{cli_helper.rb => cli/progress_helper.rb} | 52 +- lib/mastodon/{search_cli.rb => cli/search.rb} | 53 +- .../{settings_cli.rb => cli/settings.rb} | 16 +- .../{statuses_cli.rb => cli/statuses.rb} | 17 +- .../{upgrade_cli.rb => cli/upgrade.rb} | 35 +- lib/mastodon/migration_helpers.rb | 4 +- lib/mastodon/migration_warning.rb | 55 + lib/mastodon/premailer_webpack_strategy.rb | 4 +- lib/mastodon/redis_config.rb | 22 +- lib/mastodon/sidekiq_middleware.rb | 4 +- lib/mastodon/snowflake.rb | 4 +- lib/mastodon/version.rb | 6 +- lib/paperclip/attachment_extensions.rb | 10 +- lib/paperclip/color_extractor.rb | 40 +- lib/paperclip/gif_transcoder.rb | 6 +- lib/paperclip/type_corrector.rb | 2 +- lib/public_file_server_middleware.rb | 43 + lib/rails/engine_extensions.rb | 4 +- lib/sanitize_ext/sanitize_config.rb | 51 +- lib/tasks/assets.rake | 16 +- lib/tasks/auto_annotate_models.rake | 70 +- lib/tasks/branding.rake | 4 +- lib/tasks/db.rake | 14 +- lib/tasks/emojis.rake | 12 +- lib/tasks/glitchsoc.rake | 8 +- lib/tasks/mastodon.rake | 80 +- lib/tasks/repo.rake | 10 +- lib/tasks/statistics.rake | 4 +- lib/tasks/tests.rake | 21 +- .../post_deployment_migration/migration.rb | 8 - lib/terrapin/multi_pipe_extensions.rb | 2 +- package.json | 223 +- postcss.config.js | 8 +- scalingo.json | 2 +- spec/chewy/accounts_index_spec.rb | 31 + spec/chewy/statuses_index_spec.rb | 31 + spec/chewy/tags_index_spec.rb | 31 + spec/config/initializers/rack_attack_spec.rb | 76 +- spec/controllers/.rubocop.yml | 6 + spec/controllers/about_controller_spec.rb | 4 +- spec/controllers/accounts_controller_spec.rb | 30 +- .../activitypub/claims_controller_spec.rb | 19 + .../collections_controller_spec.rb | 18 +- ...lowers_synchronizations_controller_spec.rb | 36 +- .../activitypub/inboxes_controller_spec.rb | 14 +- .../activitypub/outboxes_controller_spec.rb | 14 +- .../activitypub/replies_controller_spec.rb | 5 +- .../admin/account_actions_controller_spec.rb | 35 + ...ccount_moderation_notes_controller_spec.rb | 10 +- .../admin/accounts_controller_spec.rb | 176 +- .../admin/action_logs_controller_spec.rb | 2 +- .../admin/announcements_controller_spec.rb | 102 + .../controllers/admin/base_controller_spec.rb | 12 +- ...ec.rb => change_emails_controller_spec.rb} | 17 +- .../admin/confirmations_controller_spec.rb | 10 +- .../admin/custom_emojis_controller_spec.rb | 4 +- .../admin/dashboard_controller_spec.rb | 10 +- .../admin/disputes/appeals_controller_spec.rb | 20 +- .../admin/domain_allows_controller_spec.rb | 6 +- .../admin/domain_blocks_controller_spec.rb | 158 +- .../email_domain_blocks_controller_spec.rb | 2 +- .../export_domain_allows_controller_spec.rb | 10 +- .../export_domain_blocks_controller_spec.rb | 10 +- .../follow_recommendations_controller_spec.rb | 21 + .../admin/instances_controller_spec.rb | 14 +- .../admin/invites_controller_spec.rb | 6 +- .../admin/ip_blocks_controller_spec.rb | 54 + .../admin/relationships_controller_spec.rb | 23 + .../admin/relays_controller_spec.rb | 100 + .../admin/report_notes_controller_spec.rb | 16 +- .../admin/reports/actions_controller_spec.rb | 49 +- .../admin/reports_controller_spec.rb | 11 +- .../admin/resets_controller_spec.rb | 3 + .../admin/roles_controller_spec.rb | 18 +- .../admin/rules_controller_spec.rb | 85 + .../admin/settings/about_controller_spec.rb | 21 + .../settings/appearance_controller_spec.rb | 21 + .../settings/branding_controller_spec.rb | 2 +- .../content_retention_controller_spec.rb | 21 + .../settings/discovery_controller_spec.rb | 21 + .../settings/registrations_controller_spec.rb | 21 + .../admin/site_uploads_controller_spec.rb | 23 + .../admin/statuses_controller_spec.rb | 18 +- .../controllers/admin/tags_controller_spec.rb | 2 +- .../preview_card_providers_controller_spec.rb | 21 + .../admin/trends/links_controller_spec.rb | 21 + .../admin/trends/statuses_controller_spec.rb | 21 + .../admin/trends/tags_controller_spec.rb | 21 + ...controller.rb => roles_controller_spec.rb} | 8 +- ..._factor_authentications_controller_spec.rb | 8 +- .../admin/warning_presets_controller_spec.rb | 85 + .../admin/webhooks/secrets_controller_spec.rb | 23 + .../admin/webhooks_controller_spec.rb | 99 + spec/controllers/api/base_controller_spec.rb | 26 +- .../controllers/api/oembed_controller_spec.rb | 8 +- .../accounts/credentials_controller_spec.rb | 21 +- .../familiar_followers_controller_spec.rb | 23 + .../accounts/featured_tags_controller_spec.rb | 23 + .../follower_accounts_controller_spec.rb | 6 +- .../following_accounts_controller_spec.rb | 6 +- .../identity_proofs_controller_spec.rb | 23 + .../api/v1/accounts/lists_controller_spec.rb | 2 + .../api/v1/accounts/lookup_controller_spec.rb | 23 + .../api/v1/accounts/notes_controller_spec.rb | 4 +- .../api/v1/accounts/pins_controller_spec.rb | 2 +- .../accounts/relationships_controller_spec.rb | 47 +- .../api/v1/accounts/search_controller_spec.rb | 4 +- .../v1/accounts/statuses_controller_spec.rb | 32 +- .../api/v1/accounts_controller_spec.rb | 34 +- .../admin/account_actions_controller_spec.rb | 69 - .../api/v1/admin/accounts_controller_spec.rb | 22 +- .../v1/admin/dimensions_controller_spec.rb | 23 + .../v1/admin/domain_allows_controller_spec.rb | 130 - .../v1/admin/domain_blocks_controller_spec.rb | 179 - .../api/v1/admin/measures_controller_spec.rb | 23 + .../api/v1/admin/reports_controller_spec.rb | 109 - .../api/v1/admin/retention_controller_spec.rb | 23 + .../preview_card_providers_controller_spec.rb | 52 + .../v1/admin/trends/links_controller_spec.rb | 52 + .../admin/trends/statuses_controller_spec.rb | 52 + .../v1/admin/trends/tags_controller_spec.rb | 52 + .../reactions_controller_spec.rb | 6 +- .../api/v1/announcements_controller_spec.rb | 6 +- .../v1/apps/credentials_controller_spec.rb | 43 - .../api/v1/blocks_controller_spec.rb | 16 +- .../api/v1/bookmarks_controller_spec.rb | 18 +- .../api/v1/conversations_controller_spec.rb | 26 +- .../api/v1/custom_emojis_controller_spec.rb | 2 +- .../api/v1/directories_controller_spec.rb | 128 + .../api/v1/domain_blocks_controller_spec.rb | 75 - .../emails/confirmations_controller_spec.rb | 92 +- .../api/v1/endorsements_controller_spec.rb | 2 +- .../api/v1/favourites_controller_spec.rb | 18 +- .../suggestions_controller_spec.rb | 23 + .../api/v1/filters_controller_spec.rb | 4 +- .../api/v1/follow_requests_controller_spec.rb | 70 - .../api/v1/followed_tags_controller_spec.rb | 4 +- .../v1/instances/activity_controller_spec.rb | 4 +- .../domain_blocks_controller_spec.rb | 16 + .../extended_descriptions_controller_spec.rb | 15 + .../api/v1/instances/peers_controller_spec.rb | 4 +- .../privacy_policies_controller_spec.rb | 15 + .../api/v1/instances/rules_controller_spec.rb | 15 + .../translation_languages_controller_spec.rb | 31 + .../api/v1/instances_controller_spec.rb | 2 +- .../api/v1/lists/accounts_controller_spec.rb | 47 +- .../api/v1/lists_controller_spec.rb | 78 - .../api/v1/markers_controller_spec.rb | 8 +- .../api/v1/media_controller_spec.rb | 24 +- .../api/v1/mutes_controller_spec.rb | 16 +- .../api/v1/notifications_controller_spec.rb | 55 +- .../api/v1/polls/votes_controller_spec.rb | 4 +- .../api/v1/polls_controller_spec.rb | 4 +- .../api/v1/preferences_controller_spec.rb | 23 + .../v1/push/subscriptions_controller_spec.rb | 28 +- .../api/v1/reports_controller_spec.rb | 9 +- .../v1/scheduled_statuses_controller_spec.rb | 23 + .../favourited_by_accounts_controller_spec.rb | 8 +- .../v1/statuses/histories_controller_spec.rb | 1 + .../reblogged_by_accounts_controller_spec.rb | 8 +- .../statuses/translations_controller_spec.rb | 34 + .../api/v1/statuses_controller_spec.rb | 27 +- .../api/v1/streaming_controller_spec.rb | 4 +- .../api/v1/suggestions_controller_spec.rb | 35 - .../api/v1/tags_controller_spec.rb | 86 - .../v1/timelines/direct_controller_spec.rb | 2 +- .../api/v1/timelines/home_controller_spec.rb | 2 +- .../api/v1/timelines/list_controller_spec.rb | 4 +- .../api/v1/trends/links_controller_spec.rb | 15 + .../api/v1/trends/statuses_controller_spec.rb | 15 + .../api/v1/trends/tags_controller_spec.rb | 2 +- .../api/v2/admin/accounts_controller_spec.rb | 30 +- .../v2/filters/keywords_controller_spec.rb | 8 +- .../v2/filters/statuses_controller_spec.rb | 8 +- .../api/v2/filters_controller_spec.rb | 8 +- .../api/v2/instances_controller_spec.rb | 22 + .../api/v2/search_controller_spec.rb | 35 +- .../api/v2/suggestions_controller_spec.rb | 22 + .../api/web/embeds_controller_spec.rb | 12 +- .../web/push_subscriptions_controller_spec.rb | 6 +- .../application_controller_spec.rb | 60 +- .../auth/challenges_controller_spec.rb | 2 +- .../auth/confirmations_controller_spec.rb | 2 +- .../auth/passwords_controller_spec.rb | 2 +- .../auth/registrations_controller_spec.rb | 174 +- .../auth/sessions_controller_spec.rb | 83 +- .../controllers/auth/setup_controller_spec.rb | 25 + .../authorize_interactions_controller_spec.rb | 11 +- .../account_controller_concern_spec.rb | 2 +- .../concerns/accountable_concern_spec.rb | 16 +- .../concerns/cache_concern_spec.rb | 6 +- .../concerns/challengable_concern_spec.rb | 14 +- .../export_controller_concern_spec.rb | 4 +- spec/controllers/concerns/localized_spec.rb | 8 +- .../concerns/rate_limit_headers_spec.rb | 4 +- .../concerns/signature_verification_spec.rb | 24 +- .../concerns/user_tracking_concern_spec.rb | 2 +- .../controllers/custom_css_controller_spec.rb | 30 + .../disputes/appeals_controller_spec.rb | 7 +- .../disputes/strikes_controller_spec.rb | 6 +- spec/controllers/emojis_controller_spec.rb | 5 +- .../filters/statuses_controller_spec.rb | 47 + spec/controllers/filters_controller_spec.rb | 34 + .../follower_accounts_controller_spec.rb | 17 +- .../following_accounts_controller_spec.rb | 17 +- spec/controllers/health_controller_spec.rb | 9 +- spec/controllers/home_controller_spec.rb | 8 +- .../instance_actors_controller_spec.rb | 10 +- spec/controllers/intents_controller_spec.rb | 5 +- spec/controllers/invites_controller_spec.rb | 49 +- spec/controllers/manifests_controller_spec.rb | 17 +- .../oauth/authorizations_controller_spec.rb | 7 +- ...authorized_applications_controller_spec.rb | 7 +- .../oauth/tokens_controller_spec.rb | 2 +- spec/controllers/privacy_controller_spec.rb | 14 + .../relationships_controller_spec.rb | 74 +- .../settings/aliases_controller_spec.rb | 74 + .../settings/applications_controller_spec.rb | 61 +- .../settings/deletes_controller_spec.rb | 13 +- .../blocked_accounts_controller_spec.rb | 2 + .../blocked_domains_controller_spec.rb | 20 + .../exports/bookmarks_controller_spec.rb | 2 + .../following_accounts_controller_spec.rb | 2 + .../settings/exports/lists_controller_spec.rb | 21 + .../exports/muted_accounts_controller_spec.rb | 2 + .../settings/exports_controller_spec.rb | 12 +- .../settings/featured_tags_controller_spec.rb | 43 +- .../settings/flavours_controller_spec.rb | 3 +- .../settings/imports_controller_spec.rb | 323 +- .../login_activities_controller_spec.rb | 27 + .../migration/redirects_controller_spec.rb | 70 + .../settings/migrations_controller_spec.rb | 22 +- .../settings/pictures_controller_spec.rb | 52 + .../preferences/appearance_controller_spec.rb | 41 + .../preferences/base_controller_spec.rb | 11 + .../notifications_controller_spec.rb | 27 +- .../preferences/other_controller_spec.rb | 29 +- .../settings/profiles_controller_spec.rb | 25 +- .../settings/sessions_controller_spec.rb | 7 +- .../confirmations_controller_spec.rb | 32 +- .../webauthn_credentials_controller_spec.rb | 16 +- ..._authentication_methods_controller_spec.rb | 101 +- spec/controllers/shares_controller_spec.rb | 7 +- .../statuses_cleanup_controller_spec.rb | 29 +- spec/controllers/statuses_controller_spec.rb | 476 +- spec/controllers/tags_controller_spec.rb | 49 +- .../well_known/host_meta_controller_spec.rb | 16 +- .../well_known/nodeinfo_controller_spec.rb | 9 +- .../well_known/webfinger_controller_spec.rb | 54 +- spec/fabricators/access_grant_fabricator.rb | 2 + spec/fabricators/access_token_fabricator.rb | 2 + .../accessible_access_token_fabricator.rb | 2 + spec/fabricators/account_alias_fabricator.rb | 5 - .../account_deletion_request_fabricator.rb | 3 - .../account_domain_block_fabricator.rb | 4 +- spec/fabricators/account_fabricator.rb | 2 + .../account_migration_fabricator.rb | 3 + .../account_moderation_note_fabricator.rb | 7 +- spec/fabricators/account_note_fabricator.rb | 8 +- spec/fabricators/account_pin_fabricator.rb | 7 +- spec/fabricators/account_stat_fabricator.rb | 10 +- ...ount_statuses_cleanup_policy_fabricator.rb | 4 +- .../account_tag_stat_fabricator.rb | 3 - .../fabricators/account_warning_fabricator.rb | 4 +- .../account_warning_preset_fabricator.rb | 4 +- .../admin_action_log_fabricator.rb | 6 +- spec/fabricators/announcement_fabricator.rb | 2 + .../announcement_mute_fabricator.rb | 4 - .../announcement_reaction_fabricator.rb | 5 - spec/fabricators/appeal_fabricator.rb | 2 + spec/fabricators/application_fabricator.rb | 2 + spec/fabricators/backup_fabricator.rb | 4 +- spec/fabricators/block_fabricator.rb | 6 +- spec/fabricators/bookmark_fabricator.rb | 6 +- spec/fabricators/bulk_import_fabricator.rb | 12 + .../fabricators/bulk_import_row_fabricator.rb | 6 + .../canonical_email_block_fabricator.rb | 6 +- .../conversation_account_fabricator.rb | 6 - spec/fabricators/conversation_fabricator.rb | 2 + .../conversation_mute_fabricator.rb | 2 - .../custom_emoji_category_fabricator.rb | 3 - spec/fabricators/custom_emoji_fabricator.rb | 4 +- spec/fabricators/custom_filter_fabricator.rb | 4 +- .../custom_filter_keyword_fabricator.rb | 6 +- .../custom_filter_status_fabricator.rb | 6 +- spec/fabricators/device_fabricator.rb | 6 +- spec/fabricators/domain_allow_fabricator.rb | 4 +- spec/fabricators/domain_block_fabricator.rb | 2 + .../email_domain_block_fabricator.rb | 2 + .../encrypted_message_fabricator.rb | 11 +- spec/fabricators/favourite_fabricator.rb | 6 +- spec/fabricators/featured_tag_fabricator.rb | 9 +- spec/fabricators/follow_fabricator.rb | 6 +- ...w_recommendation_suppression_fabricator.rb | 3 - spec/fabricators/follow_request_fabricator.rb | 6 +- spec/fabricators/identity_fabricator.rb | 8 +- spec/fabricators/import_fabricator.rb | 2 - spec/fabricators/invite_fabricator.rb | 4 +- spec/fabricators/ip_block_fabricator.rb | 6 - spec/fabricators/list_account_fabricator.rb | 8 +- spec/fabricators/list_fabricator.rb | 6 +- spec/fabricators/login_activity_fabricator.rb | 4 +- spec/fabricators/marker_fabricator.rb | 4 +- .../media_attachment_fabricator.rb | 4 +- spec/fabricators/mention_fabricator.rb | 6 +- spec/fabricators/mute_fabricator.rb | 6 +- spec/fabricators/notification_fabricator.rb | 6 +- spec/fabricators/one_time_key_fabricator.rb | 6 +- spec/fabricators/poll_fabricator.rb | 6 +- spec/fabricators/poll_vote_fabricator.rb | 6 +- spec/fabricators/preview_card_fabricator.rb | 2 + .../preview_card_provider_fabricator.rb | 5 + spec/fabricators/relay_fabricator.rb | 4 +- spec/fabricators/report_fabricator.rb | 8 +- spec/fabricators/report_note_fabricator.rb | 8 +- spec/fabricators/rule_fabricator.rb | 2 + .../scheduled_status_fabricator.rb | 4 +- .../session_activation_fabricator.rb | 6 +- spec/fabricators/setting_fabricator.rb | 1 + spec/fabricators/site_upload_fabricator.rb | 5 +- spec/fabricators/status_edit_fabricator.rb | 7 - spec/fabricators/status_fabricator.rb | 6 +- spec/fabricators/status_pin_fabricator.rb | 6 +- spec/fabricators/status_stat_fabricator.rb | 10 +- spec/fabricators/system_key_fabricator.rb | 3 +- spec/fabricators/tag_fabricator.rb | 2 + spec/fabricators/tag_follow_fabricator.rb | 4 +- .../unavailable_domain_fabricator.rb | 4 +- spec/fabricators/user_fabricator.rb | 7 +- .../user_invite_request_fabricator.rb | 4 - spec/fabricators/user_role_fabricator.rb | 10 +- .../web_push_subscription_fabricator.rb | 2 + spec/fabricators/web_setting_fabricator.rb | 2 - .../webauthn_credential_fabricator.rb | 2 + spec/fabricators/webhook_fabricator.rb | 2 + spec/fabricators_spec.rb | 12 + spec/features/admin/domain_blocks_spec.rb | 78 + spec/features/captcha_spec.rb | 35 + spec/features/log_in_spec.rb | 30 +- spec/features/oauth_spec.rb | 190 + spec/features/profile_spec.rb | 18 +- .../fixtures/files/empty.csv | 0 spec/fixtures/files/following_accounts.csv | 5 + spec/fixtures/files/lists.csv | 3 + spec/fixtures/files/muted_accounts.csv | 5 + ...ost_deployment_migration_generator_spec.rb | 27 + spec/helpers/accounts_helper_spec.rb | 12 +- .../account_moderation_notes_helper_spec.rb | 16 +- ...per_spec.rb => action_logs_helper_spec.rb} | 2 +- spec/helpers/admin/dashboard_helper_spec.rb | 69 + spec/helpers/admin/filter_helper_spec.rb | 2 + .../admin/trends/statuses_helper_spec.rb | 54 + spec/helpers/application_helper_spec.rb | 230 +- spec/helpers/flashes_helper_spec.rb | 2 +- spec/helpers/formatting_helper_spec.rb | 2 +- spec/helpers/home_helper_spec.rb | 116 +- spec/helpers/jsonld_helper_spec.rb | 24 +- spec/helpers/languages_helper_spec.rb | 48 +- spec/helpers/media_component_helper_spec.rb | 86 + spec/helpers/react_component_helper_spec.rb | 45 + spec/helpers/routing_helper_spec.rb | 6 +- spec/helpers/settings_helper_spec.rb | 37 + spec/helpers/statuses_helper_spec.rb | 126 +- spec/lib/account_reach_finder_spec.rb | 53 + spec/lib/activitypub/activity/accept_spec.rb | 8 +- spec/lib/activitypub/activity/add_spec.rb | 16 +- .../lib/activitypub/activity/announce_spec.rb | 32 +- spec/lib/activitypub/activity/block_spec.rb | 2 + spec/lib/activitypub/activity/create_spec.rb | 97 +- spec/lib/activitypub/activity/delete_spec.rb | 4 + spec/lib/activitypub/activity/flag_spec.rb | 36 +- spec/lib/activitypub/activity/follow_spec.rb | 22 +- spec/lib/activitypub/activity/like_spec.rb | 2 + spec/lib/activitypub/activity/move_spec.rb | 4 +- spec/lib/activitypub/activity/reject_spec.rb | 20 +- spec/lib/activitypub/activity/remove_spec.rb | 2 + spec/lib/activitypub/activity/undo_spec.rb | 8 +- spec/lib/activitypub/activity/update_spec.rb | 6 +- spec/lib/activitypub/adapter_spec.rb | 82 +- spec/lib/activitypub/dereferencer_spec.rb | 6 +- .../activitypub/linked_data_signature_spec.rb | 6 +- spec/lib/activitypub/tag_manager_spec.rb | 2 + .../instance_accounts_dimension_spec.rb | 18 + .../instance_languages_dimension_spec.rb | 18 + .../dimension/languages_dimension_spec.rb | 18 + .../dimension/servers_dimension_spec.rb | 18 + .../software_versions_dimension_spec.rb | 18 + .../dimension/sources_dimension_spec.rb | 18 + .../dimension/space_usage_dimension_spec.rb | 18 + .../dimension/tag_languages_dimension_spec.rb | 18 + .../dimension/tag_servers_dimension_spec.rb | 18 + .../measure/active_users_measure_spec.rb | 17 + .../measure/instance_accounts_measure_spec.rb | 46 + .../instance_followers_measure_spec.rb | 48 + .../measure/instance_follows_measure_spec.rb | 48 + ...instance_media_attachments_measure_spec.rb | 49 + .../measure/instance_reports_measure_spec.rb | 45 + .../measure/instance_statuses_measure_spec.rb | 45 + .../measure/interactions_measure_spec.rb | 17 + .../metrics/measure/new_users_measure_spec.rb | 17 + .../measure/opened_reports_measure_spec.rb | 17 + .../measure/resolved_reports_measure_spec.rb | 17 + .../measure/tag_accounts_measure_spec.rb | 19 + .../measure/tag_servers_measure_spec.rb | 19 + .../metrics/measure/tag_uses_measure_spec.rb | 19 + .../lib/admin/system_check/base_check_spec.rb | 27 + .../database_schema_check_spec.rb | 45 + .../system_check/elasticsearch_check_spec.rb | 100 + .../system_check/media_privacy_check_spec.rb | 33 + spec/lib/admin/system_check/message_spec.rb | 14 + .../admin/system_check/rules_check_spec.rb | 53 + .../sidekiq_process_check_spec.rb | 45 + spec/lib/admin/system_check_spec.rb | 15 + spec/lib/advanced_text_formatter_spec.rb | 144 +- .../shared_connection_pool_spec.rb | 16 +- .../shared_timed_stack_spec.rb | 28 +- spec/lib/emoji_formatter_spec.rb | 20 +- spec/lib/entity_cache_spec.rb | 8 +- spec/lib/extractor_spec.rb | 28 +- spec/lib/fast_ip_map_spec.rb | 2 +- spec/lib/feed_manager_spec.rb | 227 +- spec/lib/hash_object_spec.rb | 9 - spec/lib/html_aware_formatter_spec.rb | 16 +- .../importer/accounts_index_importer_spec.rb | 16 + spec/lib/importer/base_importer_spec.rb | 14 + .../importer/statuses_index_importer_spec.rb | 16 + spec/lib/importer/tags_index_importer_spec.rb | 16 + spec/lib/link_details_extractor_spec.rb | 134 +- spec/lib/mastodon/cli/accounts_spec.rb | 1364 +++ spec/lib/mastodon/cli/cache_spec.rb | 71 + .../cli/canonical_email_blocks_spec.rb | 60 + spec/lib/mastodon/cli/domains_spec.rb | 12 + .../mastodon/cli/email_domain_blocks_spec.rb | 12 + spec/lib/mastodon/cli/emoji_spec.rb | 12 + spec/lib/mastodon/cli/feeds_spec.rb | 68 + spec/lib/mastodon/cli/ip_blocks_spec.rb | 298 + spec/lib/mastodon/cli/main_spec.rb | 20 + spec/lib/mastodon/cli/maintenance_spec.rb | 12 + spec/lib/mastodon/cli/media_spec.rb | 12 + spec/lib/mastodon/cli/preview_cards_spec.rb | 12 + spec/lib/mastodon/cli/search_spec.rb | 12 + spec/lib/mastodon/cli/settings_spec.rb | 70 + spec/lib/mastodon/cli/statuses_spec.rb | 12 + spec/lib/mastodon/cli/upgrade_spec.rb | 12 + spec/lib/mastodon/migration_warning_spec.rb | 34 + spec/lib/ostatus/tag_manager_spec.rb | 28 +- spec/lib/plain_text_formatter_spec.rb | 65 +- spec/lib/request_pool_spec.rb | 27 +- spec/lib/request_spec.rb | 22 +- spec/lib/sanitize_config_spec.rb | 12 + spec/lib/scope_transformer_spec.rb | 22 +- spec/lib/search_query_transformer_spec.rb | 18 + spec/lib/settings/extend_spec.rb | 16 - spec/lib/settings/scoped_settings_spec.rb | 35 - spec/lib/status_cache_hydrator_spec.rb | 14 +- spec/lib/status_filter_spec.rb | 21 +- spec/lib/status_reach_finder_spec.rb | 18 +- spec/lib/suspicious_sign_in_detector_spec.rb | 10 +- spec/lib/tag_manager_spec.rb | 24 +- spec/lib/text_formatter_spec.rb | 154 +- spec/lib/translation_service/deepl_spec.rb | 100 + .../libre_translate_spec.rb | 72 + spec/lib/user_settings_decorator_spec.rb | 84 - spec/lib/vacuum/access_tokens_vacuum_spec.rb | 12 + spec/lib/vacuum/backups_vacuum_spec.rb | 6 +- spec/lib/vacuum/feeds_vacuum_spec.rb | 2 + spec/lib/vacuum/imports_vacuum_spec.rb | 19 + .../vacuum/media_attachments_vacuum_spec.rb | 5 +- spec/lib/vacuum/preview_cards_vacuum_spec.rb | 6 +- spec/lib/vacuum/statuses_vacuum_spec.rb | 6 +- spec/lib/vacuum/system_keys_vacuum_spec.rb | 2 + spec/lib/webfinger_resource_spec.rb | 58 +- spec/lib/webhooks/payload_renderer_spec.rb | 30 + spec/locales/i18n_spec.rb | 35 + spec/mailers/admin_mailer_spec.rb | 64 +- spec/mailers/notification_mailer_spec.rb | 70 +- spec/mailers/previews/admin_mailer_preview.rb | 2 + .../previews/notification_mailer_preview.rb | 2 + spec/mailers/previews/user_mailer_preview.rb | 2 + spec/mailers/user_mailer_spec.rb | 121 +- spec/models/account/field_spec.rb | 52 +- spec/models/account_alias_spec.rb | 5 +- spec/models/account_conversation_spec.rb | 20 +- spec/models/account_deletion_request_spec.rb | 4 +- spec/models/account_domain_block_spec.rb | 12 +- spec/models/account_filter_spec.rb | 47 + spec/models/account_migration_spec.rb | 12 +- spec/models/account_moderation_note_spec.rb | 4 +- spec/models/account_spec.rb | 209 +- .../account_statuses_cleanup_policy_spec.rb | 125 +- spec/models/account_statuses_filter_spec.rb | 4 +- spec/models/account_warning_preset_spec.rb | 17 + spec/models/admin/account_action_spec.rb | 45 +- spec/models/admin/action_log_spec.rb | 2 +- spec/models/admin/appeal_filter_spec.rb | 16 + spec/models/announcement_mute_spec.rb | 4 +- spec/models/announcement_reaction_spec.rb | 4 +- spec/models/announcement_spec.rb | 4 +- spec/models/appeal_spec.rb | 37 +- spec/models/backup_spec.rb | 4 +- spec/models/block_spec.rb | 21 +- spec/models/canonical_email_block_spec.rb | 4 +- spec/models/concerns/account_counters_spec.rb | 2 + .../concerns/account_interactions_spec.rb | 371 +- spec/models/concerns/remotable_spec.rb | 59 +- .../concerns/status_threading_concern_spec.rb | 42 +- spec/models/conversation_mute_spec.rb | 4 +- spec/models/conversation_spec.rb | 4 +- spec/models/custom_emoji_category_spec.rb | 13 +- spec/models/custom_emoji_filter_spec.rb | 42 +- spec/models/custom_emoji_spec.rb | 38 +- spec/models/custom_filter_keyword_spec.rb | 4 +- spec/models/custom_filter_spec.rb | 4 +- spec/models/device_spec.rb | 5 +- spec/models/domain_allow_spec.rb | 17 +- spec/models/domain_block_spec.rb | 63 +- spec/models/email_domain_block_spec.rb | 25 +- spec/models/encrypted_message_spec.rb | 5 +- spec/models/export_spec.rb | 30 +- spec/models/extended_description_spec.rb | 29 + spec/models/favourite_spec.rb | 12 +- spec/models/featured_tag_spec.rb | 4 +- .../follow_recommendation_suppression_spec.rb | 4 +- spec/models/follow_request_spec.rb | 46 +- spec/models/follow_spec.rb | 17 +- spec/models/form/account_batch_spec.rb | 63 + spec/models/form/admin_settings_spec.rb | 36 + spec/models/form/import_spec.rb | 318 + .../form/status_filter_batch_action_spec.rb | 13 + spec/models/home_feed_spec.rb | 8 +- spec/models/identity_spec.rb | 6 +- spec/models/import_spec.rb | 26 +- spec/models/invite_spec.rb | 4 +- spec/models/ip_block_spec.rb | 14 +- spec/models/list_account_spec.rb | 4 +- spec/models/list_spec.rb | 4 +- spec/models/login_activity_spec.rb | 5 +- spec/models/marker_spec.rb | 15 +- spec/models/media_attachment_spec.rb | 80 +- spec/models/mention_spec.rb | 9 +- spec/models/mute_spec.rb | 4 +- spec/models/notification_spec.rb | 144 +- spec/models/one_time_key_spec.rb | 20 +- spec/models/poll_spec.rb | 31 +- spec/models/poll_vote_spec.rb | 51 +- spec/models/preview_card_provider_spec.rb | 42 + spec/models/preview_card_spec.rb | 4 +- spec/models/preview_card_trend_spec.rb | 4 +- spec/models/privacy_policy_spec.rb | 28 + spec/models/public_feed_spec.rb | 60 +- spec/models/relationship_filter_spec.rb | 2 +- spec/models/relay_spec.rb | 4 +- spec/models/remote_follow_spec.rb | 22 +- spec/models/report_filter_spec.rb | 8 +- spec/models/report_spec.rb | 27 +- spec/models/rule_spec.rb | 18 +- spec/models/scheduled_status_spec.rb | 4 +- spec/models/session_activation_spec.rb | 34 +- spec/models/setting_spec.rb | 48 +- spec/models/site_upload_spec.rb | 4 +- spec/models/status_edit_spec.rb | 12 +- spec/models/status_pin_spec.rb | 22 +- spec/models/status_spec.rb | 182 +- spec/models/status_stat_spec.rb | 4 +- spec/models/status_trend_spec.rb | 4 +- spec/models/system_key_spec.rb | 5 +- spec/models/tag_feed_spec.rb | 68 +- spec/models/tag_follow_spec.rb | 4 +- spec/models/tag_spec.rb | 7 +- spec/models/trends/statuses_spec.rb | 53 +- spec/models/trends/tags_spec.rb | 30 +- spec/models/unavailable_domain_spec.rb | 4 +- spec/models/user_invite_request_spec.rb | 4 +- spec/models/user_role_spec.rb | 20 +- spec/models/user_settings/namespace_spec.rb | 25 + spec/models/user_settings/setting_spec.rb | 106 + spec/models/user_settings_spec.rb | 120 + spec/models/user_spec.rb | 242 +- spec/models/web/push_subscription_spec.rb | 30 +- spec/models/web/setting_spec.rb | 4 +- spec/models/webauthn_credentials_spec.rb | 8 +- spec/models/webhook_spec.rb | 4 +- .../account_moderation_note_policy_spec.rb | 14 +- spec/policies/account_policy_spec.rb | 72 +- .../account_warning_preset_policy_spec.rb | 24 + spec/policies/admin/status_policy_spec.rb | 51 + spec/policies/announcement_policy_spec.rb | 24 + spec/policies/appeal_policy_spec.rb | 51 + spec/policies/backup_policy_spec.rb | 10 +- .../canonical_email_block_policy_spec.rb | 24 + spec/policies/custom_emoji_policy_spec.rb | 8 +- spec/policies/delivery_policy_spec.rb | 24 + spec/policies/domain_block_policy_spec.rb | 4 +- .../email_domain_block_policy_spec.rb | 6 +- .../follow_recommendation_policy_spec.rb | 24 + spec/policies/instance_policy_spec.rb | 4 +- spec/policies/invite_policy_spec.rb | 18 +- spec/policies/ip_block_policy_spec.rb | 24 + spec/policies/preview_card_policy_spec.rb | 24 + .../preview_card_provider_policy_spec.rb | 24 + spec/policies/relay_policy_spec.rb | 4 +- spec/policies/report_note_policy_spec.rb | 26 +- spec/policies/report_policy_spec.rb | 4 +- spec/policies/rule_policy_spec.rb | 24 + spec/policies/settings_policy_spec.rb | 6 +- spec/policies/status_policy_spec.rb | 188 +- spec/policies/tag_policy_spec.rb | 6 +- spec/policies/user_policy_spec.rb | 36 +- spec/policies/webhook_policy_spec.rb | 40 + .../account_relationships_presenter_spec.rb | 18 +- .../familiar_followers_presenter_spec.rb | 6 +- spec/presenters/instance_presenter_spec.rb | 58 +- .../status_relationships_presenter_spec.rb | 16 +- spec/rails_helper.rb | 72 +- spec/requests/anonymous_cookies_spec.rb | 44 + spec/requests/api/v1/accounts_show_spec.rb | 53 + .../api/v1/admin/account_actions_spec.rb | 154 + .../v1/admin/canonical_email_blocks_spec.rb | 285 + .../api/v1/admin/domain_allows_spec.rb | 194 + .../api/v1/admin/domain_blocks_spec.rb | 264 + .../api/v1/admin/email_domain_blocks_spec.rb | 211 + spec/requests/api/v1/admin/ip_blocks_spec.rb | 255 + spec/requests/api/v1/admin/reports_spec.rb | 272 + spec/requests/api/v1/apps/credentials_spec.rb | 44 + .../api/v1/apps_spec.rb} | 63 +- spec/requests/api/v1/domain_blocks_spec.rb | 125 + spec/requests/api/v1/featured_tags_spec.rb | 193 + spec/requests/api/v1/follow_requests_spec.rb | 119 + spec/requests/api/v1/lists_spec.rb | 247 + spec/requests/api/v1/suggestions_spec.rb | 103 + spec/requests/api/v1/tags_spec.rb | 169 + spec/requests/backups_spec.rb | 26 + spec/requests/cache_spec.rb | 685 ++ spec/requests/catch_all_route_request_spec.rb | 26 +- spec/requests/follower_accounts_spec.rb | 13 + spec/requests/following_accounts_spec.rb | 13 + spec/requests/host_meta_request_spec.rb | 12 +- spec/requests/link_headers_spec.rb | 4 +- spec/requests/localization_spec.rb | 12 +- spec/requests/webfinger_request_spec.rb | 2 + spec/routing/accounts_routing_spec.rb | 2 + spec/routing/api_routing_spec.rb | 72 +- spec/routing/well_known_routes_spec.rb | 22 +- .../activitypub/device_serializer_spec.rb | 20 + .../{note_spec.rb => note_serializer_spec.rb} | 26 +- .../one_time_key_serializer_spec.rb | 20 + .../activitypub/undo_like_serializer_spec.rb | 20 + ...spec.rb => update_poll_serializer_spec.rb} | 6 +- .../activitypub/vote_serializer_spec.rb | 20 + .../rest/account_serializer_spec.rb | 47 + .../rest/encrypted_message_serializer_spec.rb | 20 + .../rest/instance_serializer_spec.rb | 20 + .../rest/keys/claim_result_serializer_spec.rb | 20 + .../rest/keys/device_serializer_spec.rb | 20 + .../rest/keys/query_result_serializer_spec.rb | 20 + .../rest/suggestion_serializer_spec.rb | 26 + spec/services/account_search_service_spec.rb | 12 +- .../account_statuses_cleanup_service_spec.rb | 10 +- .../fetch_featured_collection_service_spec.rb | 42 +- ...h_featured_tags_collection_service_spec.rb | 10 +- .../fetch_remote_account_service_spec.rb | 4 +- .../fetch_remote_actor_service_spec.rb | 4 +- .../fetch_remote_key_service_spec.rb | 4 +- .../fetch_remote_status_service_spec.rb | 138 +- .../activitypub/fetch_replies_service_spec.rb | 8 +- .../process_account_service_spec.rb | 36 +- .../process_collection_service_spec.rb | 124 +- .../process_status_update_service_spec.rb | 100 +- .../synchronize_followers_service_spec.rb | 8 +- ..._block_domain_from_account_service_spec.rb | 6 +- spec/services/after_block_service_spec.rb | 2 + spec/services/app_sign_up_service_spec.rb | 6 +- .../services/authorize_follow_service_spec.rb | 6 +- spec/services/backup_service_spec.rb | 101 + .../batched_remove_status_service_spec.rb | 22 +- spec/services/block_domain_service_spec.rb | 24 +- spec/services/block_service_spec.rb | 6 +- .../bootstrap_timeline_service_spec.rb | 7 +- spec/services/bulk_import_row_service_spec.rb | 167 + spec/services/bulk_import_service_spec.rb | 417 + .../clear_domain_media_service_spec.rb | 18 +- spec/services/delete_account_service_spec.rb | 24 +- .../services/fan_out_on_write_service_spec.rb | 9 +- spec/services/favourite_service_spec.rb | 10 +- spec/services/fetch_link_card_service_spec.rb | 40 +- spec/services/fetch_oembed_service_spec.rb | 27 +- .../fetch_remote_status_service_spec.rb | 9 +- spec/services/fetch_resource_service_spec.rb | 21 +- spec/services/follow_service_spec.rb | 12 +- spec/services/import_service_spec.rb | 59 +- spec/services/mute_service_spec.rb | 2 + spec/services/notify_service_spec.rb | 27 +- spec/services/post_status_service_spec.rb | 75 +- spec/services/precompute_feed_service_spec.rb | 7 +- .../services/process_mentions_service_spec.rb | 35 +- spec/services/purge_domain_service_spec.rb | 16 +- spec/services/reblog_service_spec.rb | 33 +- spec/services/reject_follow_service_spec.rb | 6 +- ... => remove_from_followers_service_spec.rb} | 8 +- spec/services/remove_status_service_spec.rb | 72 +- spec/services/report_service_spec.rb | 25 +- spec/services/resolve_account_service_spec.rb | 42 +- spec/services/resolve_url_service_spec.rb | 42 +- spec/services/search_service_spec.rb | 30 +- spec/services/suspend_account_service_spec.rb | 8 +- .../services/translate_status_service_spec.rb | 235 + spec/services/unallow_domain_service_spec.rb | 22 +- spec/services/unblock_service_spec.rb | 6 +- spec/services/unfollow_service_spec.rb | 6 +- spec/services/unmute_service_spec.rb | 4 +- .../unsuspend_account_service_spec.rb | 20 +- spec/services/update_account_service_spec.rb | 4 +- spec/services/update_status_service_spec.rb | 15 +- spec/services/verify_link_service_spec.rb | 28 +- spec/spec_helper.rb | 29 +- spec/support/examples/api.rb | 23 + spec/support/examples/lib/admin/checks.rb | 21 + .../examples/lib/settings/scoped_settings.rb | 74 - .../lib/settings/settings_extended.rb | 15 - .../models/concerns/account_avatar.rb | 4 +- .../models/concerns/account_header.rb | 2 +- .../matchers/json/match_json_schema.rb | 2 + .../model/model_have_error_on_field.rb | 8 +- spec/support/stories/profile_stories.rb | 4 +- .../blacklisted_email_validator_spec.rb | 12 +- .../disallowed_hashtags_validator_spec.rb | 12 +- spec/validators/email_mx_validator_spec.rb | 67 +- .../validators/follow_limit_validator_spec.rb | 18 +- spec/validators/note_length_validator_spec.rb | 18 +- spec/validators/poll_validator_spec.rb | 11 +- .../status_length_validator_spec.rb | 32 +- spec/validators/status_pin_validator_spec.rb | 18 +- .../unique_username_validator_spec.rb | 20 +- .../unreserved_username_validator_spec.rb | 20 +- spec/validators/url_validator_spec.rb | 10 +- spec/views/statuses/show.html.haml_spec.rb | 14 +- .../distribute_poll_update_worker_spec.rb | 2 + .../activitypub/distribution_worker_spec.rb | 4 +- .../move_distribution_worker_spec.rb | 10 +- .../activitypub/processing_worker_spec.rb | 5 +- .../status_update_distribution_worker_spec.rb | 2 + .../update_distribution_worker_spec.rb | 2 + .../admin/account_deletion_worker_spec.rb | 19 + .../workers/admin/domain_purge_worker_spec.rb | 2 +- spec/workers/bulk_import_worker_spec.rb | 26 + spec/workers/cache_buster_worker_spec.rb | 19 + spec/workers/domain_block_worker_spec.rb | 4 +- .../workers/domain_clear_media_worker_spec.rb | 4 +- spec/workers/feed_insert_worker_spec.rb | 18 +- spec/workers/import/row_worker_spec.rb | 127 + spec/workers/move_worker_spec.rb | 119 +- .../poll_expiration_notify_worker_spec.rb | 72 + .../workers/post_process_media_worker_spec.rb | 13 + ...lish_scheduled_announcement_worker_spec.rb | 2 +- spec/workers/push_conversation_worker_spec.rb | 13 + .../push_encrypted_message_worker_spec.rb | 13 + spec/workers/push_update_worker_spec.rb | 16 + spec/workers/redownload_avatar_worker_spec.rb | 13 + spec/workers/redownload_header_worker_spec.rb | 13 + spec/workers/refollow_worker_spec.rb | 3 +- spec/workers/regeneration_worker_spec.rb | 4 +- .../remove_featured_tag_worker_spec.rb | 15 + spec/workers/resolve_account_worker_spec.rb | 13 + ...ccounts_statuses_cleanup_scheduler_spec.rb | 154 +- .../follow_recommendations_scheduler_spec.rb | 43 + .../scheduler/indexing_scheduler_spec.rb | 13 + .../instance_refresh_scheduler_spec.rb | 13 + .../scheduler/ip_cleanup_scheduler_spec.rb | 13 + .../scheduler/pghero_scheduler_spec.rb | 13 + .../scheduled_statuses_scheduler_spec.rb | 13 + .../suspended_user_cleanup_scheduler_spec.rb | 13 + .../trends/refresh_scheduler_spec.rb | 13 + .../review_notifications_scheduler_spec.rb | 13 + .../scheduler/user_cleanup_scheduler_spec.rb | 41 + .../scheduler/vacuum_scheduler_spec.rb | 13 + spec/workers/unfollow_follow_worker_spec.rb | 4 +- .../unpublish_announcement_worker_spec.rb | 13 + .../verify_account_links_worker_spec.rb | 13 + .../web/push_notification_worker_spec.rb | 2 +- spec/workers/webhooks/delivery_worker_spec.rb | 13 + streaming/index.js | 360 +- stylelint.config.js | 26 +- tsconfig.json | 29 + yarn.lock | 8937 +++++++++-------- 3329 files changed, 85020 insertions(+), 49761 deletions(-) create mode 100644 .bundler-audit.yml delete mode 100644 .circleci/config.yml delete mode 100644 .codeclimate.yml delete mode 100644 .deepsource.toml create mode 100644 .devcontainer/welcome-message.txt create mode 100644 .github/renovate.json5 create mode 100644 .github/workflows/build-nightly.yml create mode 100644 .github/workflows/haml-lint-problem-matcher.json create mode 100644 .github/workflows/lint-css.yml create mode 100644 .github/workflows/lint-haml.yml create mode 100644 .github/workflows/lint-js.yml create mode 100644 .github/workflows/lint-md.yml create mode 100644 .github/workflows/lint-ruby.yml delete mode 100644 .github/workflows/linter.yml create mode 100644 .github/workflows/test-js.yml create mode 100644 .github/workflows/test-migrations-one-step.yml create mode 100644 .github/workflows/test-migrations-two-step.yml create mode 100644 .github/workflows/test-ruby.yml create mode 100644 .haml-lint_todo.yml create mode 100755 .husky/pre-commit create mode 100644 .rubocop_todo.yml create mode 100644 app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb create mode 100644 app/controllers/api/v1/instances/translation_languages_controller.rb create mode 100644 app/controllers/backups_controller.rb create mode 100644 app/controllers/concerns/api_caching_concern.rb create mode 100644 app/controllers/mail_subscriptions_controller.rb create mode 100644 app/controllers/settings/preferences/base_controller.rb delete mode 100644 app/controllers/settings/preferences_controller.rb create mode 100644 app/controllers/settings/verifications_controller.rb delete mode 100644 app/helpers/admin/announcements_helper.rb create mode 100644 app/helpers/media_component_helper.rb create mode 100644 app/helpers/react_component_helper.rb delete mode 100644 app/javascript/flavours/glitch/actions/app.js create mode 100644 app/javascript/flavours/glitch/actions/app.ts delete mode 100644 app/javascript/flavours/glitch/actions/modal.js create mode 100644 app/javascript/flavours/glitch/actions/modal.ts delete mode 100644 app/javascript/flavours/glitch/base_polyfills.js rename app/javascript/{mastodon/blurhash.js => flavours/glitch/blurhash.ts} (81%) rename app/javascript/{mastodon/compare_id.js => flavours/glitch/compare_id.ts} (75%) rename app/javascript/flavours/glitch/components/{account.js => account.jsx} (93%) rename app/javascript/flavours/glitch/components/admin/{Counter.js => Counter.jsx} (90%) rename app/javascript/flavours/glitch/components/admin/{Dimension.js => Dimension.jsx} (93%) create mode 100644 app/javascript/flavours/glitch/components/admin/ImpactReport.jsx rename app/javascript/flavours/glitch/components/admin/{ReportReasonSelector.js => ReportReasonSelector.jsx} (92%) rename app/javascript/flavours/glitch/components/admin/{Retention.js => Retention.jsx} (97%) rename app/javascript/flavours/glitch/components/admin/{Trends.js => Trends.jsx} (95%) delete mode 100644 app/javascript/flavours/glitch/components/animated_number.js create mode 100644 app/javascript/flavours/glitch/components/animated_number.tsx rename app/javascript/{mastodon/components/attachment_list.js => flavours/glitch/components/attachment_list.jsx} (95%) rename app/javascript/flavours/glitch/components/{autosuggest_emoji.js => autosuggest_emoji.jsx} (89%) delete mode 100644 app/javascript/flavours/glitch/components/autosuggest_hashtag.js create mode 100644 app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx rename app/javascript/flavours/glitch/components/{autosuggest_input.js => autosuggest_input.jsx} (94%) rename app/javascript/flavours/glitch/components/{autosuggest_textarea.js => autosuggest_textarea.jsx} (95%) delete mode 100644 app/javascript/flavours/glitch/components/avatar.js create mode 100644 app/javascript/flavours/glitch/components/avatar.tsx rename app/javascript/flavours/glitch/components/{avatar_composite.js => avatar_composite.jsx} (83%) rename app/javascript/flavours/glitch/components/{avatar_overlay.js => avatar_overlay.jsx} (91%) delete mode 100644 app/javascript/flavours/glitch/components/blurhash.js create mode 100644 app/javascript/flavours/glitch/components/blurhash.tsx rename app/javascript/flavours/glitch/components/{button.js => button.jsx} (90%) rename app/javascript/flavours/glitch/components/{check.js => check.jsx} (89%) create mode 100644 app/javascript/flavours/glitch/components/circular_progress.tsx rename app/javascript/flavours/glitch/components/{column.js => column.jsx} (70%) rename app/javascript/{mastodon/components/column_back_button.js => flavours/glitch/components/column_back_button.jsx} (69%) delete mode 100644 app/javascript/flavours/glitch/components/column_back_button_slim.js create mode 100644 app/javascript/flavours/glitch/components/column_back_button_slim.jsx rename app/javascript/flavours/glitch/components/{column_header.js => column_header.jsx} (90%) rename app/javascript/flavours/glitch/components/{common_counter.js => common_counter.jsx} (98%) rename app/javascript/flavours/glitch/components/{dismissable_banner.js => dismissable_banner.jsx} (85%) delete mode 100644 app/javascript/flavours/glitch/components/display_name.js create mode 100644 app/javascript/flavours/glitch/components/display_name.tsx delete mode 100644 app/javascript/flavours/glitch/components/domain.js create mode 100644 app/javascript/flavours/glitch/components/domain.tsx rename app/javascript/{mastodon/components/dropdown_menu.js => flavours/glitch/components/dropdown_menu.jsx} (89%) rename app/javascript/flavours/glitch/components/edited_timestamp/{index.js => index.jsx} (81%) rename app/javascript/flavours/glitch/components/{error_boundary.js => error_boundary.jsx} (97%) delete mode 100644 app/javascript/flavours/glitch/components/gifv.js create mode 100644 app/javascript/flavours/glitch/components/gifv.tsx rename app/javascript/flavours/glitch/components/{hashtag.js => hashtag.jsx} (90%) delete mode 100644 app/javascript/flavours/glitch/components/icon.js create mode 100644 app/javascript/flavours/glitch/components/icon.tsx rename app/javascript/flavours/glitch/components/{icon_button.js => icon_button.tsx} (59%) delete mode 100644 app/javascript/flavours/glitch/components/icon_with_badge.js create mode 100644 app/javascript/flavours/glitch/components/icon_with_badge.tsx delete mode 100644 app/javascript/flavours/glitch/components/image.js rename app/javascript/flavours/glitch/components/{inline_account.js => inline_account.jsx} (76%) rename app/javascript/flavours/glitch/components/{intersection_observer_article.js => intersection_observer_article.jsx} (92%) rename app/javascript/flavours/glitch/components/{link.js => link.jsx} (96%) delete mode 100644 app/javascript/flavours/glitch/components/load_gap.js create mode 100644 app/javascript/flavours/glitch/components/load_gap.tsx delete mode 100644 app/javascript/flavours/glitch/components/load_more.js create mode 100644 app/javascript/flavours/glitch/components/load_more.tsx delete mode 100644 app/javascript/flavours/glitch/components/load_pending.js create mode 100644 app/javascript/flavours/glitch/components/load_pending.tsx delete mode 100644 app/javascript/flavours/glitch/components/loading_indicator.js create mode 100644 app/javascript/flavours/glitch/components/loading_indicator.tsx delete mode 100644 app/javascript/flavours/glitch/components/logo.js create mode 100644 app/javascript/flavours/glitch/components/logo.jsx rename app/javascript/flavours/glitch/components/{media_attachments.js => media_attachments.jsx} (86%) rename app/javascript/flavours/glitch/components/{media_gallery.js => media_gallery.jsx} (79%) delete mode 100644 app/javascript/flavours/glitch/components/missing_indicator.js rename app/javascript/flavours/glitch/components/{modal_root.js => modal_root.jsx} (96%) rename app/javascript/flavours/glitch/components/{navigation_portal.js => navigation_portal.jsx} (79%) delete mode 100644 app/javascript/flavours/glitch/components/not_signed_in_indicator.js create mode 100644 app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx rename app/javascript/flavours/glitch/components/{notification_purge_buttons.js => notification_purge_buttons.jsx} (74%) rename app/javascript/flavours/glitch/components/{permalink.js => permalink.jsx} (75%) delete mode 100644 app/javascript/flavours/glitch/components/picture_in_picture_placeholder.js create mode 100644 app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx rename app/javascript/flavours/glitch/components/{poll.js => poll.jsx} (82%) delete mode 100644 app/javascript/flavours/glitch/components/radio_button.js create mode 100644 app/javascript/flavours/glitch/components/radio_button.tsx rename app/javascript/flavours/glitch/components/{regeneration_indicator.js => regeneration_indicator.jsx} (96%) delete mode 100644 app/javascript/flavours/glitch/components/relative_timestamp.js create mode 100644 app/javascript/flavours/glitch/components/relative_timestamp.tsx rename app/javascript/flavours/glitch/components/{scrollable_list.js => scrollable_list.jsx} (83%) rename app/javascript/flavours/glitch/components/{server_banner.js => server_banner.jsx} (87%) create mode 100644 app/javascript/flavours/glitch/components/server_hero_image.tsx rename app/javascript/flavours/glitch/components/{setting_text.js => setting_text.jsx} (88%) rename app/javascript/flavours/glitch/components/{short_number.js => short_number.jsx} (94%) delete mode 100644 app/javascript/flavours/glitch/components/skeleton.js create mode 100644 app/javascript/flavours/glitch/components/skeleton.tsx delete mode 100644 app/javascript/flavours/glitch/components/spoilers.js rename app/javascript/flavours/glitch/components/{status.js => status.jsx} (88%) rename app/javascript/flavours/glitch/components/{status_action_bar.js => status_action_bar.jsx} (92%) rename app/javascript/flavours/glitch/components/{status_content.js => status_content.jsx} (87%) rename app/javascript/flavours/glitch/components/{status_header.js => status_header.jsx} (87%) rename app/javascript/flavours/glitch/components/{status_icons.js => status_icons.jsx} (86%) rename app/javascript/flavours/glitch/components/{status_list.js => status_list.jsx} (93%) rename app/javascript/flavours/glitch/components/{status_prepend.js => status_prepend.jsx} (95%) rename app/javascript/flavours/glitch/components/{status_visibility_icon.js => status_visibility_icon.jsx} (91%) delete mode 100644 app/javascript/flavours/glitch/components/timeline_hint.js create mode 100644 app/javascript/flavours/glitch/components/timeline_hint.tsx rename app/javascript/flavours/glitch/containers/{account_container.js => account_container.jsx} (81%) delete mode 100644 app/javascript/flavours/glitch/containers/admin_component.js create mode 100644 app/javascript/flavours/glitch/containers/admin_component.jsx delete mode 100644 app/javascript/flavours/glitch/containers/compose_container.js create mode 100644 app/javascript/flavours/glitch/containers/compose_container.jsx delete mode 100644 app/javascript/flavours/glitch/containers/domain_container.js rename app/javascript/{mastodon/containers/domain_container.js => flavours/glitch/containers/domain_container.jsx} (52%) rename app/javascript/flavours/glitch/containers/{mastodon.js => mastodon.jsx} (82%) rename app/javascript/flavours/glitch/containers/{media_container.js => media_container.jsx} (80%) rename app/javascript/flavours/glitch/features/about/{index.js => index.jsx} (91%) rename app/javascript/flavours/glitch/features/account/components/{account_note.js => account_note.jsx} (91%) rename app/javascript/flavours/glitch/features/account/components/{action_bar.js => action_bar.jsx} (79%) rename app/javascript/flavours/glitch/features/account/components/{featured_tags.js => featured_tags.jsx} (97%) rename app/javascript/{mastodon/features/account/components/follow_request_note.js => flavours/glitch/features/account/components/follow_request_note.jsx} (95%) rename app/javascript/flavours/glitch/features/account/components/{header.js => header.jsx} (94%) rename app/javascript/flavours/glitch/features/account/components/{profile_column_header.js => profile_column_header.jsx} (83%) rename app/javascript/flavours/glitch/features/account/{navigation.js => navigation.jsx} (87%) rename app/javascript/flavours/glitch/features/account_gallery/components/{media_item.js => media_item.jsx} (93%) rename app/javascript/flavours/glitch/features/account_gallery/{index.js => index.jsx} (86%) rename app/javascript/flavours/glitch/features/account_timeline/components/{header.js => header.jsx} (95%) rename app/javascript/flavours/glitch/features/account_timeline/components/{limited_account_hint.js => limited_account_hint.jsx} (84%) create mode 100644 app/javascript/flavours/glitch/features/account_timeline/components/memorial_note.jsx rename app/javascript/flavours/glitch/features/account_timeline/components/{moved_note.js => moved_note.jsx} (78%) rename app/javascript/flavours/glitch/features/account_timeline/containers/{header_container.js => header_container.jsx} (63%) rename app/javascript/flavours/glitch/features/account_timeline/{index.js => index.jsx} (92%) rename app/javascript/flavours/glitch/features/audio/{index.js => index.jsx} (88%) rename app/javascript/flavours/glitch/features/blocks/{index.js => index.jsx} (92%) rename app/javascript/flavours/glitch/features/bookmarked_statuses/{index.js => index.jsx} (92%) rename app/javascript/flavours/glitch/features/closed_registrations_modal/{index.js => index.jsx} (97%) rename app/javascript/flavours/glitch/features/community_timeline/components/{column_settings.js => column_settings.jsx} (91%) rename app/javascript/flavours/glitch/features/community_timeline/{index.js => index.jsx} (91%) rename app/javascript/flavours/glitch/features/compose/components/{action_bar.js => action_bar.jsx} (92%) rename app/javascript/flavours/glitch/features/compose/components/{autosuggest_account.js => autosuggest_account.jsx} (79%) rename app/javascript/flavours/glitch/features/compose/components/{character_counter.js => character_counter.jsx} (83%) rename app/javascript/flavours/glitch/features/compose/components/{compose_form.js => compose_form.jsx} (91%) rename app/javascript/flavours/glitch/features/compose/components/{dropdown.js => dropdown.jsx} (92%) rename app/javascript/flavours/glitch/features/compose/components/{dropdown_menu.js => dropdown_menu.jsx} (86%) rename app/javascript/flavours/glitch/features/compose/components/{emoji_picker_dropdown.js => emoji_picker_dropdown.jsx} (94%) rename app/javascript/flavours/glitch/features/compose/components/{header.js => header.jsx} (92%) rename app/javascript/flavours/glitch/features/compose/components/{language_dropdown.js => language_dropdown.jsx} (93%) rename app/javascript/flavours/glitch/features/compose/components/{navigation_bar.js => navigation_bar.jsx} (95%) rename app/javascript/flavours/glitch/features/compose/components/{options.js => options.jsx} (94%) rename app/javascript/flavours/glitch/features/compose/components/{poll_form.js => poll_form.jsx} (87%) rename app/javascript/flavours/glitch/features/compose/components/{privacy_dropdown.js => privacy_dropdown.jsx} (91%) rename app/javascript/flavours/glitch/features/compose/components/{publisher.js => publisher.jsx} (81%) rename app/javascript/flavours/glitch/features/compose/components/{reply_indicator.js => reply_indicator.jsx} (94%) rename app/javascript/flavours/glitch/features/compose/components/{search.js => search.jsx} (91%) rename app/javascript/flavours/glitch/features/compose/components/{search_results.js => search_results.jsx} (94%) rename app/javascript/flavours/glitch/features/compose/components/{text_icon_button.js => text_icon_button.jsx} (88%) rename app/javascript/flavours/glitch/features/compose/components/{textarea_icons.js => textarea_icons.jsx} (92%) rename app/javascript/flavours/glitch/features/compose/components/{upload.js => upload.jsx} (89%) rename app/javascript/flavours/glitch/features/compose/components/{upload_form.js => upload_form.jsx} (97%) rename app/javascript/flavours/glitch/features/compose/components/{upload_progress.js => upload_progress.jsx} (88%) rename app/javascript/flavours/glitch/features/compose/components/{warning.js => warning.jsx} (88%) rename app/javascript/flavours/glitch/features/compose/containers/{sensitive_button_container.js => sensitive_button_container.jsx} (96%) rename app/javascript/flavours/glitch/features/compose/containers/{warning_container.js => warning_container.jsx} (83%) rename app/javascript/flavours/glitch/features/compose/{index.js => index.jsx} (92%) rename app/javascript/flavours/glitch/features/direct_timeline/components/{column_settings.js => column_settings.jsx} (92%) rename app/javascript/flavours/glitch/features/direct_timeline/components/{conversation.js => conversation.jsx} (93%) rename app/javascript/flavours/glitch/features/direct_timeline/components/{conversations_list.js => conversations_list.jsx} (86%) rename app/javascript/flavours/glitch/features/direct_timeline/{index.js => index.jsx} (89%) rename app/javascript/flavours/glitch/features/directory/components/{account_card.js => account_card.jsx} (86%) rename app/javascript/flavours/glitch/features/directory/{index.js => index.jsx} (93%) rename app/javascript/flavours/glitch/features/domain_blocks/{index.js => index.jsx} (93%) create mode 100644 app/javascript/flavours/glitch/features/emoji/emoji_compressed.d.ts delete mode 100644 app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js create mode 100644 app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.ts rename app/javascript/flavours/glitch/features/explore/components/{story.js => story.jsx} (85%) rename app/javascript/flavours/glitch/features/explore/{index.js => index.jsx} (90%) rename app/javascript/flavours/glitch/features/explore/{links.js => links.jsx} (83%) rename app/javascript/flavours/glitch/features/explore/{results.js => results.jsx} (92%) rename app/javascript/flavours/glitch/features/explore/{statuses.js => statuses.jsx} (80%) rename app/javascript/flavours/glitch/features/explore/{suggestions.js => suggestions.jsx} (88%) rename app/javascript/flavours/glitch/features/explore/{tags.js => tags.jsx} (82%) rename app/javascript/flavours/glitch/features/favourited_statuses/{index.js => index.jsx} (92%) rename app/javascript/flavours/glitch/features/favourites/{index.js => index.jsx} (90%) rename app/javascript/flavours/glitch/features/filters/{added_to_filter.js => added_to_filter.jsx} (92%) rename app/javascript/flavours/glitch/features/filters/{select_filter.js => select_filter.jsx} (93%) create mode 100644 app/javascript/flavours/glitch/features/firehose/index.jsx rename app/javascript/flavours/glitch/features/follow_recommendations/components/{account.js => account.jsx} (87%) rename app/javascript/flavours/glitch/features/follow_recommendations/{index.js => index.jsx} (97%) rename app/javascript/flavours/glitch/features/follow_requests/components/{account_authorize.js => account_authorize.jsx} (87%) rename app/javascript/flavours/glitch/features/follow_requests/{index.js => index.jsx} (89%) rename app/javascript/flavours/glitch/features/followed_tags/{index.js => index.jsx} (97%) rename app/javascript/flavours/glitch/features/followers/{index.js => index.jsx} (93%) rename app/javascript/flavours/glitch/features/following/{index.js => index.jsx} (93%) delete mode 100644 app/javascript/flavours/glitch/features/generic_not_found/index.js rename app/javascript/flavours/glitch/features/getting_started/components/{announcements.js => announcements.jsx} (95%) rename app/javascript/flavours/glitch/features/getting_started/components/{trends.js => trends.jsx} (97%) rename app/javascript/flavours/glitch/features/getting_started/{index.js => index.jsx} (92%) rename app/javascript/flavours/glitch/features/getting_started_misc/{index.js => index.jsx} (52%) rename app/javascript/{mastodon/features/hashtag_timeline/components/column_settings.js => flavours/glitch/features/hashtag_timeline/components/column_settings.jsx} (96%) rename app/javascript/flavours/glitch/features/hashtag_timeline/{index.js => index.jsx} (91%) rename app/javascript/flavours/glitch/features/home_timeline/components/{column_settings.js => column_settings.jsx} (92%) create mode 100644 app/javascript/flavours/glitch/features/home_timeline/components/explore_prompt.jsx rename app/javascript/flavours/glitch/features/home_timeline/{index.js => index.jsx} (66%) rename app/javascript/flavours/glitch/features/interaction_modal/{index.js => index.jsx} (88%) rename app/javascript/flavours/glitch/features/keyboard_shortcuts/{index.js => index.jsx} (98%) rename app/javascript/{mastodon/features/list_adder/components/account.js => flavours/glitch/features/list_adder/components/account.jsx} (83%) rename app/javascript/flavours/glitch/features/list_adder/components/{list.js => list.jsx} (87%) rename app/javascript/{mastodon/features/list_adder/index.js => flavours/glitch/features/list_adder/index.jsx} (95%) rename app/javascript/flavours/glitch/features/list_editor/components/{account.js => account.jsx} (87%) rename app/javascript/flavours/glitch/features/list_editor/components/{edit_list_form.js => edit_list_form.jsx} (86%) rename app/javascript/flavours/glitch/features/list_editor/components/{search.js => search.jsx} (86%) rename app/javascript/flavours/glitch/features/list_editor/{index.js => index.jsx} (94%) rename app/javascript/flavours/glitch/features/list_timeline/{index.js => index.jsx} (72%) rename app/javascript/flavours/glitch/features/lists/components/{new_list_form.js => new_list_form.jsx} (88%) rename app/javascript/flavours/glitch/features/lists/{index.js => index.jsx} (93%) rename app/javascript/flavours/glitch/features/local_settings/{index.js => index.jsx} (89%) rename app/javascript/flavours/glitch/features/local_settings/navigation/{index.js => index.jsx} (93%) rename app/javascript/flavours/glitch/features/local_settings/navigation/item/{index.js => index.jsx} (89%) rename app/javascript/flavours/glitch/features/local_settings/page/deprecated_item/{index.js => index.jsx} (93%) rename app/javascript/flavours/glitch/features/local_settings/page/{index.js => index.jsx} (92%) rename app/javascript/flavours/glitch/features/local_settings/page/item/{index.js => index.jsx} (85%) rename app/javascript/flavours/glitch/features/mutes/{index.js => index.jsx} (93%) rename app/javascript/flavours/glitch/features/notifications/components/{admin_report.js => admin_report.jsx} (86%) rename app/javascript/flavours/glitch/features/notifications/components/{admin_signup.js => admin_signup.jsx} (95%) rename app/javascript/flavours/glitch/features/notifications/components/{clear_column_button.js => clear_column_button.jsx} (61%) rename app/javascript/flavours/glitch/features/notifications/components/{column_settings.js => column_settings.jsx} (99%) rename app/javascript/{mastodon/features/notifications/components/filter_bar.js => flavours/glitch/features/notifications/components/filter_bar.jsx} (95%) rename app/javascript/flavours/glitch/features/notifications/components/{follow.js => follow.jsx} (95%) rename app/javascript/flavours/glitch/features/notifications/components/{follow_request.js => follow_request.jsx} (90%) rename app/javascript/flavours/glitch/features/notifications/components/{grant_permission_button.js => grant_permission_button.jsx} (71%) rename app/javascript/flavours/glitch/features/notifications/components/{notification.js => notification.jsx} (97%) rename app/javascript/flavours/glitch/features/notifications/components/{notifications_permission_banner.js => notifications_permission_banner.jsx} (84%) rename app/javascript/flavours/glitch/features/notifications/components/{overlay.js => overlay.jsx} (93%) rename app/javascript/flavours/glitch/features/notifications/components/{pill_bar_button.js => pill_bar_button.jsx} (87%) rename app/javascript/flavours/glitch/features/notifications/components/{report.js => report.jsx} (81%) rename app/javascript/flavours/glitch/features/notifications/components/{setting_toggle.js => setting_toggle.jsx} (91%) rename app/javascript/flavours/glitch/features/notifications/{index.js => index.jsx} (91%) rename app/javascript/flavours/glitch/features/picture_in_picture/components/{footer.js => footer.jsx} (86%) rename app/javascript/flavours/glitch/features/picture_in_picture/components/{header.js => header.jsx} (82%) rename app/javascript/flavours/glitch/features/picture_in_picture/{index.js => index.jsx} (93%) rename app/javascript/flavours/glitch/features/pinned_accounts_editor/{index.js => index.jsx} (94%) rename app/javascript/flavours/glitch/features/pinned_statuses/{index.js => index.jsx} (75%) rename app/javascript/flavours/glitch/features/privacy_policy/{index.js => index.jsx} (90%) rename app/javascript/flavours/glitch/features/public_timeline/components/{column_settings.js => column_settings.jsx} (93%) rename app/javascript/flavours/glitch/features/public_timeline/{index.js => index.jsx} (91%) rename app/javascript/flavours/glitch/features/reblogs/{index.js => index.jsx} (90%) rename app/javascript/flavours/glitch/features/report/{category.js => category.jsx} (95%) rename app/javascript/flavours/glitch/features/report/{comment.js => comment.jsx} (92%) rename app/javascript/flavours/glitch/features/report/components/{option.js => option.jsx} (92%) rename app/javascript/flavours/glitch/features/report/components/{status_check_box.js => status_check_box.jsx} (84%) rename app/javascript/flavours/glitch/features/report/{rules.js => rules.jsx} (92%) rename app/javascript/flavours/glitch/features/report/{statuses.js => statuses.jsx} (89%) rename app/javascript/flavours/glitch/features/report/{thanks.js => thanks.jsx} (95%) rename app/javascript/flavours/glitch/features/standalone/compose/{index.js => index.jsx} (80%) rename app/javascript/flavours/glitch/features/status/components/{action_bar.js => action_bar.jsx} (92%) rename app/javascript/flavours/glitch/features/status/components/{card.js => card.jsx} (76%) rename app/javascript/flavours/glitch/features/status/components/{detailed_status.js => detailed_status.jsx} (87%) rename app/javascript/flavours/glitch/features/status/{index.js => index.jsx} (83%) rename app/javascript/flavours/glitch/features/subscribed_languages_modal/{index.js => index.jsx} (96%) rename app/javascript/flavours/glitch/features/ui/components/{actions_modal.js => actions_modal.jsx} (87%) rename app/javascript/flavours/glitch/features/ui/components/{audio_modal.js => audio_modal.jsx} (74%) rename app/javascript/flavours/glitch/features/ui/components/{block_modal.js => block_modal.jsx} (90%) rename app/javascript/flavours/glitch/features/ui/components/{boost_modal.js => boost_modal.jsx} (82%) rename app/javascript/flavours/glitch/features/ui/components/{bundle.js => bundle.jsx} (92%) rename app/javascript/flavours/glitch/features/ui/components/{bundle_column_error.js => bundle_column_error.jsx} (95%) rename app/javascript/flavours/glitch/features/ui/components/{bundle_modal_error.js => bundle_modal_error.jsx} (90%) rename app/javascript/flavours/glitch/features/ui/components/{column.js => column.jsx} (94%) rename app/javascript/flavours/glitch/features/ui/components/{column_header.js => column_header.jsx} (82%) rename app/javascript/flavours/glitch/features/ui/components/{column_link.js => column_link.jsx} (86%) rename app/javascript/flavours/glitch/features/ui/components/{column_loading.js => column_loading.jsx} (96%) rename app/javascript/flavours/glitch/features/ui/components/{column_subheading.js => column_subheading.jsx} (90%) rename app/javascript/flavours/glitch/features/ui/components/{columns_area.js => columns_area.jsx} (94%) rename app/javascript/flavours/glitch/features/ui/components/{compare_history_modal.js => compare_history_modal.jsx} (78%) rename app/javascript/flavours/glitch/features/ui/components/{compose_panel.js => compose_panel.jsx} (86%) rename app/javascript/flavours/glitch/features/ui/components/{confirmation_modal.js => confirmation_modal.jsx} (93%) rename app/javascript/flavours/glitch/features/ui/components/{deprecated_settings_modal.js => deprecated_settings_modal.jsx} (91%) rename app/javascript/flavours/glitch/features/ui/components/{disabled_account_banner.js => disabled_account_banner.jsx} (85%) rename app/javascript/flavours/glitch/features/ui/components/{doodle_modal.js => doodle_modal.jsx} (97%) rename app/javascript/{mastodon/features/ui/components/drawer_loading.js => flavours/glitch/features/ui/components/drawer_loading.jsx} (86%) rename app/javascript/flavours/glitch/features/ui/components/{embed_modal.js => embed_modal.jsx} (93%) rename app/javascript/flavours/glitch/features/ui/components/{favourite_modal.js => favourite_modal.jsx} (86%) rename app/javascript/flavours/glitch/features/ui/components/{filter_modal.js => filter_modal.jsx} (96%) rename app/javascript/flavours/glitch/features/ui/components/{focal_point_modal.js => focal_point_modal.jsx} (94%) rename app/javascript/flavours/glitch/features/ui/components/{follow_requests_column_link.js => follow_requests_column_link.jsx} (82%) delete mode 100644 app/javascript/flavours/glitch/features/ui/components/header.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/header.jsx rename app/javascript/flavours/glitch/features/ui/components/{image_loader.js => image_loader.jsx} (95%) rename app/javascript/{mastodon/features/ui/components/image_modal.js => flavours/glitch/features/ui/components/image_modal.jsx} (88%) rename app/javascript/flavours/glitch/features/ui/components/{link_footer.js => link_footer.jsx} (65%) rename app/javascript/flavours/glitch/features/ui/components/{list_panel.js => list_panel.jsx} (94%) rename app/javascript/flavours/glitch/features/ui/components/{media_modal.js => media_modal.jsx} (90%) rename app/javascript/{mastodon/features/ui/components/modal_loading.js => flavours/glitch/features/ui/components/modal_loading.jsx} (82%) rename app/javascript/flavours/glitch/features/ui/components/{modal_root.js => modal_root.jsx} (94%) rename app/javascript/flavours/glitch/features/ui/components/{mute_modal.js => mute_modal.jsx} (94%) rename app/javascript/flavours/glitch/features/ui/components/{navigation_panel.js => navigation_panel.jsx} (81%) rename app/javascript/flavours/glitch/features/ui/components/{onboarding_modal.js => onboarding_modal.jsx} (94%) rename app/javascript/flavours/glitch/features/ui/components/{report_modal.js => report_modal.jsx} (97%) delete mode 100644 app/javascript/flavours/glitch/features/ui/components/sign_in_banner.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx rename app/javascript/flavours/glitch/features/ui/components/{upload_area.js => upload_area.jsx} (90%) rename app/javascript/flavours/glitch/features/ui/components/{video_modal.js => video_modal.jsx} (67%) rename app/javascript/{mastodon/features/ui/components/zoomable_image.js => flavours/glitch/features/ui/components/zoomable_image.jsx} (96%) rename app/javascript/flavours/glitch/features/ui/{index.js => index.jsx} (92%) rename app/javascript/flavours/glitch/features/ui/util/{react_router_helpers.js => react_router_helpers.jsx} (98%) rename app/javascript/{mastodon/features/ui/util/reduced_motion.js => flavours/glitch/features/ui/util/reduced_motion.jsx} (93%) rename app/javascript/flavours/glitch/features/video/{index.js => index.jsx} (88%) create mode 100644 app/javascript/flavours/glitch/hooks/useHovering.ts delete mode 100644 app/javascript/flavours/glitch/is_mobile.js create mode 100644 app/javascript/flavours/glitch/is_mobile.ts delete mode 100644 app/javascript/flavours/glitch/locales/defaultMessages.json create mode 100644 app/javascript/flavours/glitch/locales/global_locale.ts create mode 100644 app/javascript/flavours/glitch/locales/index.ts create mode 100644 app/javascript/flavours/glitch/locales/intl_provider.tsx create mode 100644 app/javascript/flavours/glitch/locales/load_locale.ts delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_af.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ar.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ast.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_bg.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_bn.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_br.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ca.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ckb.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_co.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_cs.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_cy.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_da.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_de.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_el.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_en.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_eo.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_es-AR.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_es-MX.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_es.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_et.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_eu.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_fa.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_fi.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_fr.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ga.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_gd.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_gl.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_he.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_hi.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_hr.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_hu.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_hy.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_id.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_io.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_is.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_it.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ja.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ka.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_kab.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_kk.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_kn.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ko.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ku.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_kw.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_lt.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_lv.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_mk.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ml.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_mr.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ms.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_nl.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_nn.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_no.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_oc.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_pa.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_pl.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_pt-BR.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_pt-PT.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ro.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ru.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_sa.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_sc.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_si.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_sk.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_sl.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_sq.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_sr-Latn.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_sr.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_sv.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_szl.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ta.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_tai.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_te.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_th.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_tr.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_tt.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ug.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_uk.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_ur.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_vi.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_zgh.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_zh-CN.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_zh-HK.json delete mode 100644 app/javascript/flavours/glitch/locales/whitelist_zh-TW.json rename app/javascript/flavours/glitch/{main.js => main.jsx} (79%) delete mode 100644 app/javascript/flavours/glitch/middleware/errors.js delete mode 100644 app/javascript/flavours/glitch/middleware/loading_bar.js delete mode 100644 app/javascript/flavours/glitch/middleware/sounds.js rename app/javascript/flavours/glitch/packs/{admin.js => admin.jsx} (66%) rename app/javascript/flavours/glitch/packs/{public.js => public.jsx} (67%) delete mode 100644 app/javascript/flavours/glitch/packs/share.js create mode 100644 app/javascript/flavours/glitch/packs/share.jsx create mode 100644 app/javascript/flavours/glitch/packs/sign_up.js delete mode 100644 app/javascript/flavours/glitch/permissions.js create mode 100644 app/javascript/flavours/glitch/permissions.ts create mode 100644 app/javascript/flavours/glitch/polyfills/base_polyfills.ts rename app/javascript/flavours/glitch/{extra_polyfills.js => polyfills/extra_polyfills.ts} (75%) rename app/javascript/{mastodon/load_polyfills.js => flavours/glitch/polyfills/index.ts} (79%) create mode 100644 app/javascript/flavours/glitch/polyfills/intl.ts rename app/javascript/flavours/glitch/reducers/{index.js => index.ts} (73%) delete mode 100644 app/javascript/flavours/glitch/reducers/modal.js create mode 100644 app/javascript/flavours/glitch/reducers/modal.ts delete mode 100644 app/javascript/flavours/glitch/scroll.js create mode 100644 app/javascript/flavours/glitch/scroll.ts delete mode 100644 app/javascript/flavours/glitch/store/configureStore.js create mode 100644 app/javascript/flavours/glitch/store/index.ts create mode 100644 app/javascript/flavours/glitch/store/middlewares/errors.ts create mode 100644 app/javascript/flavours/glitch/store/middlewares/loading_bar.ts create mode 100644 app/javascript/flavours/glitch/store/middlewares/sounds.ts create mode 100644 app/javascript/flavours/glitch/styles/components/misc.scss create mode 100644 app/javascript/flavours/glitch/styles/rich_text.scss create mode 100644 app/javascript/flavours/glitch/types/resources.ts create mode 100644 app/javascript/flavours/glitch/types/util.ts rename app/javascript/{mastodon/utils/base64.js => flavours/glitch/utils/base64.ts} (79%) delete mode 100644 app/javascript/flavours/glitch/utils/filters.js create mode 100644 app/javascript/flavours/glitch/utils/filters.ts rename app/javascript/flavours/glitch/utils/{icons.js => icons.jsx} (78%) rename app/javascript/flavours/glitch/utils/{numbers.js => numbers.ts} (52%) delete mode 100644 app/javascript/flavours/glitch/uuid.js create mode 100644 app/javascript/flavours/glitch/uuid.ts create mode 100644 app/javascript/hooks/useHovering.ts create mode 100644 app/javascript/images/elephant_ui_conversation.svg create mode 100755 app/javascript/images/friends-cropped.png delete mode 100644 app/javascript/locales/index.js delete mode 100644 app/javascript/locales/locale-data/README.md delete mode 100644 app/javascript/locales/locale-data/oc.js delete mode 100644 app/javascript/mastodon/actions/app.js create mode 100644 app/javascript/mastodon/actions/app.ts delete mode 100644 app/javascript/mastodon/actions/modal.js create mode 100644 app/javascript/mastodon/actions/modal.ts delete mode 100644 app/javascript/mastodon/base_polyfills.js rename app/javascript/{flavours/glitch/blurhash.js => mastodon/blurhash.ts} (81%) rename app/javascript/{flavours/glitch/compare_id.js => mastodon/compare_id.ts} (74%) rename app/javascript/mastodon/components/__tests__/__snapshots__/{autosuggest_emoji-test.js.snap => autosuggest_emoji-test.jsx.snap} (100%) rename app/javascript/mastodon/components/__tests__/__snapshots__/{avatar-test.js.snap => avatar-test.jsx.snap} (100%) rename app/javascript/mastodon/components/__tests__/__snapshots__/{avatar_overlay-test.js.snap => avatar_overlay-test.jsx.snap} (86%) rename app/javascript/mastodon/components/__tests__/__snapshots__/{button-test.js.snap => button-test.jsx.snap} (100%) rename app/javascript/mastodon/components/__tests__/__snapshots__/{display_name-test.js.snap => display_name-test.jsx.snap} (100%) rename app/javascript/mastodon/components/__tests__/{autosuggest_emoji-test.js => autosuggest_emoji-test.jsx} (96%) rename app/javascript/mastodon/components/__tests__/{avatar-test.js => avatar-test.jsx} (93%) rename app/javascript/mastodon/components/__tests__/{avatar_overlay-test.js => avatar_overlay-test.jsx} (90%) rename app/javascript/mastodon/components/__tests__/{button-test.js => button-test.jsx} (98%) rename app/javascript/mastodon/components/__tests__/{display_name-test.js => display_name-test.jsx} (86%) delete mode 100644 app/javascript/mastodon/components/account.js create mode 100644 app/javascript/mastodon/components/account.jsx rename app/javascript/mastodon/components/admin/{Counter.js => Counter.jsx} (90%) rename app/javascript/mastodon/components/admin/{Dimension.js => Dimension.jsx} (94%) create mode 100644 app/javascript/mastodon/components/admin/ImpactReport.jsx rename app/javascript/mastodon/components/admin/{ReportReasonSelector.js => ReportReasonSelector.jsx} (92%) rename app/javascript/mastodon/components/admin/{Retention.js => Retention.jsx} (97%) rename app/javascript/mastodon/components/admin/{Trends.js => Trends.jsx} (95%) delete mode 100644 app/javascript/mastodon/components/animated_number.js create mode 100644 app/javascript/mastodon/components/animated_number.tsx rename app/javascript/{flavours/glitch/components/attachment_list.js => mastodon/components/attachment_list.jsx} (94%) rename app/javascript/mastodon/components/{autosuggest_emoji.js => autosuggest_emoji.jsx} (88%) delete mode 100644 app/javascript/mastodon/components/autosuggest_hashtag.js create mode 100644 app/javascript/mastodon/components/autosuggest_hashtag.tsx rename app/javascript/mastodon/components/{autosuggest_input.js => autosuggest_input.jsx} (94%) rename app/javascript/mastodon/components/{autosuggest_textarea.js => autosuggest_textarea.jsx} (95%) delete mode 100644 app/javascript/mastodon/components/avatar.js create mode 100644 app/javascript/mastodon/components/avatar.tsx rename app/javascript/mastodon/components/{avatar_composite.js => avatar_composite.jsx} (93%) delete mode 100644 app/javascript/mastodon/components/avatar_overlay.js create mode 100644 app/javascript/mastodon/components/avatar_overlay.tsx delete mode 100644 app/javascript/mastodon/components/blurhash.js create mode 100644 app/javascript/mastodon/components/blurhash.tsx rename app/javascript/mastodon/components/{button.js => button.jsx} (91%) delete mode 100644 app/javascript/mastodon/components/check.js create mode 100644 app/javascript/mastodon/components/check.tsx create mode 100644 app/javascript/mastodon/components/circular_progress.tsx rename app/javascript/mastodon/components/{column.js => column.jsx} (68%) rename app/javascript/{flavours/glitch/components/column_back_button.js => mastodon/components/column_back_button.jsx} (65%) rename app/javascript/mastodon/components/{column_back_button_slim.js => column_back_button_slim.jsx} (79%) rename app/javascript/mastodon/components/{column_header.js => column_header.jsx} (95%) rename app/javascript/mastodon/components/{common_counter.js => common_counter.jsx} (98%) rename app/javascript/mastodon/components/{dismissable_banner.js => dismissable_banner.jsx} (85%) delete mode 100644 app/javascript/mastodon/components/display_name.js create mode 100644 app/javascript/mastodon/components/display_name.tsx delete mode 100644 app/javascript/mastodon/components/domain.js create mode 100644 app/javascript/mastodon/components/domain.tsx rename app/javascript/{flavours/glitch/components/dropdown_menu.js => mastodon/components/dropdown_menu.jsx} (89%) rename app/javascript/mastodon/components/edited_timestamp/{index.js => index.jsx} (81%) create mode 100644 app/javascript/mastodon/components/empty_account.tsx rename app/javascript/mastodon/components/{error_boundary.js => error_boundary.jsx} (97%) delete mode 100644 app/javascript/mastodon/components/gifv.js create mode 100644 app/javascript/mastodon/components/gifv.tsx rename app/javascript/mastodon/components/{hashtag.js => hashtag.jsx} (89%) delete mode 100644 app/javascript/mastodon/components/icon.js create mode 100644 app/javascript/mastodon/components/icon.tsx rename app/javascript/mastodon/components/{icon_button.js => icon_button.tsx} (58%) delete mode 100644 app/javascript/mastodon/components/icon_with_badge.js create mode 100644 app/javascript/mastodon/components/icon_with_badge.tsx delete mode 100644 app/javascript/mastodon/components/image.js rename app/javascript/mastodon/components/{inline_account.js => inline_account.jsx} (76%) rename app/javascript/mastodon/components/{intersection_observer_article.js => intersection_observer_article.jsx} (91%) delete mode 100644 app/javascript/mastodon/components/load_gap.js create mode 100644 app/javascript/mastodon/components/load_gap.tsx delete mode 100644 app/javascript/mastodon/components/load_more.js create mode 100644 app/javascript/mastodon/components/load_more.tsx delete mode 100644 app/javascript/mastodon/components/load_pending.js create mode 100644 app/javascript/mastodon/components/load_pending.tsx delete mode 100644 app/javascript/mastodon/components/loading_indicator.js create mode 100644 app/javascript/mastodon/components/loading_indicator.tsx delete mode 100644 app/javascript/mastodon/components/logo.js create mode 100644 app/javascript/mastodon/components/logo.tsx rename app/javascript/mastodon/components/{media_attachments.js => media_attachments.jsx} (86%) rename app/javascript/mastodon/components/{media_gallery.js => media_gallery.jsx} (78%) delete mode 100644 app/javascript/mastodon/components/missing_indicator.js rename app/javascript/mastodon/components/{modal_root.js => modal_root.jsx} (96%) rename app/javascript/mastodon/components/{navigation_portal.js => navigation_portal.jsx} (79%) delete mode 100644 app/javascript/mastodon/components/not_signed_in_indicator.js create mode 100644 app/javascript/mastodon/components/not_signed_in_indicator.tsx delete mode 100644 app/javascript/mastodon/components/picture_in_picture_placeholder.js create mode 100644 app/javascript/mastodon/components/picture_in_picture_placeholder.jsx rename app/javascript/mastodon/components/{poll.js => poll.jsx} (82%) delete mode 100644 app/javascript/mastodon/components/radio_button.js create mode 100644 app/javascript/mastodon/components/radio_button.tsx rename app/javascript/mastodon/components/{regeneration_indicator.js => regeneration_indicator.jsx} (95%) delete mode 100644 app/javascript/mastodon/components/relative_timestamp.js create mode 100644 app/javascript/mastodon/components/relative_timestamp.tsx rename app/javascript/mastodon/components/{scrollable_list.js => scrollable_list.jsx} (90%) rename app/javascript/mastodon/components/{server_banner.js => server_banner.jsx} (87%) create mode 100644 app/javascript/mastodon/components/server_hero_image.tsx rename app/javascript/mastodon/components/{short_number.js => short_number.jsx} (94%) delete mode 100644 app/javascript/mastodon/components/skeleton.js create mode 100644 app/javascript/mastodon/components/skeleton.tsx rename app/javascript/mastodon/components/{status.js => status.jsx} (83%) rename app/javascript/mastodon/components/{status_action_bar.js => status_action_bar.jsx} (94%) rename app/javascript/mastodon/components/{status_content.js => status_content.jsx} (79%) rename app/javascript/mastodon/components/{status_list.js => status_list.jsx} (93%) delete mode 100644 app/javascript/mastodon/components/timeline_hint.js create mode 100644 app/javascript/mastodon/components/timeline_hint.tsx create mode 100644 app/javascript/mastodon/components/verified_badge.tsx rename app/javascript/mastodon/containers/{account_container.js => account_container.jsx} (80%) delete mode 100644 app/javascript/mastodon/containers/admin_component.js create mode 100644 app/javascript/mastodon/containers/admin_component.jsx delete mode 100644 app/javascript/mastodon/containers/compose_container.js create mode 100644 app/javascript/mastodon/containers/compose_container.jsx create mode 100644 app/javascript/mastodon/containers/domain_container.jsx rename app/javascript/mastodon/containers/{mastodon.js => mastodon.jsx} (82%) rename app/javascript/mastodon/containers/{media_container.js => media_container.jsx} (80%) rename app/javascript/mastodon/containers/{status_container.js => status_container.jsx} (65%) rename app/javascript/mastodon/features/about/{index.js => index.jsx} (91%) rename app/javascript/mastodon/features/account/components/{account_note.js => account_note.jsx} (93%) rename app/javascript/mastodon/features/account/components/{featured_tags.js => featured_tags.jsx} (97%) rename app/javascript/{flavours/glitch/features/account/components/follow_request_note.js => mastodon/features/account/components/follow_request_note.jsx} (94%) rename app/javascript/mastodon/features/account/components/{header.js => header.jsx} (88%) rename app/javascript/mastodon/features/account/{navigation.js => navigation.jsx} (87%) rename app/javascript/mastodon/features/account_gallery/components/{media_item.js => media_item.jsx} (89%) rename app/javascript/mastodon/features/account_gallery/{index.js => index.jsx} (88%) rename app/javascript/mastodon/features/account_timeline/components/{header.js => header.jsx} (94%) rename app/javascript/mastodon/features/account_timeline/components/{limited_account_hint.js => limited_account_hint.jsx} (84%) create mode 100644 app/javascript/mastodon/features/account_timeline/components/memorial_note.jsx rename app/javascript/mastodon/features/account_timeline/components/{moved_note.js => moved_note.jsx} (89%) rename app/javascript/mastodon/features/account_timeline/containers/{header_container.js => header_container.jsx} (60%) rename app/javascript/mastodon/features/account_timeline/{index.js => index.jsx} (92%) rename app/javascript/mastodon/features/audio/{index.js => index.jsx} (88%) rename app/javascript/mastodon/features/blocks/{index.js => index.jsx} (93%) rename app/javascript/mastodon/features/bookmarked_statuses/{index.js => index.jsx} (92%) rename app/javascript/mastodon/features/closed_registrations_modal/{index.js => index.jsx} (97%) rename app/javascript/mastodon/features/community_timeline/components/{column_settings.js => column_settings.jsx} (85%) rename app/javascript/mastodon/features/community_timeline/{index.js => index.jsx} (90%) rename app/javascript/mastodon/features/compose/components/{action_bar.js => action_bar.jsx} (92%) rename app/javascript/mastodon/features/compose/components/{autosuggest_account.js => autosuggest_account.jsx} (81%) rename app/javascript/mastodon/features/compose/components/{character_counter.js => character_counter.jsx} (83%) rename app/javascript/mastodon/features/compose/components/{compose_form.js => compose_form.jsx} (81%) rename app/javascript/mastodon/features/compose/components/{emoji_picker_dropdown.js => emoji_picker_dropdown.jsx} (94%) rename app/javascript/mastodon/features/compose/components/{language_dropdown.js => language_dropdown.jsx} (93%) rename app/javascript/mastodon/features/compose/components/{navigation_bar.js => navigation_bar.jsx} (91%) rename app/javascript/mastodon/features/compose/components/{poll_button.js => poll_button.jsx} (86%) rename app/javascript/mastodon/features/compose/components/{poll_form.js => poll_form.jsx} (87%) rename app/javascript/mastodon/features/compose/components/{privacy_dropdown.js => privacy_dropdown.jsx} (92%) rename app/javascript/mastodon/features/compose/components/{reply_indicator.js => reply_indicator.jsx} (89%) delete mode 100644 app/javascript/mastodon/features/compose/components/search.js create mode 100644 app/javascript/mastodon/features/compose/components/search.jsx rename app/javascript/mastodon/features/compose/components/{search_results.js => search_results.jsx} (94%) rename app/javascript/mastodon/features/compose/components/{text_icon_button.js => text_icon_button.jsx} (88%) rename app/javascript/mastodon/features/compose/components/{upload.js => upload.jsx} (93%) rename app/javascript/mastodon/features/compose/components/{upload_button.js => upload_button.jsx} (93%) rename app/javascript/mastodon/features/compose/components/{upload_form.js => upload_form.jsx} (97%) rename app/javascript/mastodon/features/compose/components/{upload_progress.js => upload_progress.jsx} (89%) rename app/javascript/mastodon/features/compose/components/{warning.js => warning.jsx} (88%) rename app/javascript/mastodon/features/compose/containers/{sensitive_button_container.js => sensitive_button_container.jsx} (93%) rename app/javascript/mastodon/features/compose/containers/{warning_container.js => warning_container.jsx} (70%) rename app/javascript/mastodon/features/compose/{index.js => index.jsx} (92%) rename app/javascript/mastodon/features/direct_timeline/components/{conversation.js => conversation.jsx} (95%) rename app/javascript/mastodon/features/direct_timeline/components/{conversations_list.js => conversations_list.jsx} (86%) rename app/javascript/mastodon/features/direct_timeline/{index.js => index.jsx} (88%) rename app/javascript/mastodon/features/directory/components/{account_card.js => account_card.jsx} (86%) rename app/javascript/mastodon/features/directory/{index.js => index.jsx} (93%) rename app/javascript/mastodon/features/domain_blocks/{index.js => index.jsx} (93%) create mode 100644 app/javascript/mastodon/features/emoji/emoji_compressed.d.ts delete mode 100644 app/javascript/mastodon/features/emoji/emoji_mart_data_light.js create mode 100644 app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts rename app/javascript/mastodon/features/explore/components/{story.js => story.jsx} (86%) rename app/javascript/mastodon/features/explore/{index.js => index.jsx} (90%) rename app/javascript/mastodon/features/explore/{links.js => links.jsx} (84%) rename app/javascript/mastodon/features/explore/{results.js => results.jsx} (92%) rename app/javascript/mastodon/features/explore/{statuses.js => statuses.jsx} (80%) rename app/javascript/mastodon/features/explore/{suggestions.js => suggestions.jsx} (86%) rename app/javascript/mastodon/features/explore/{tags.js => tags.jsx} (82%) rename app/javascript/mastodon/features/favourited_statuses/{index.js => index.jsx} (92%) rename app/javascript/mastodon/features/favourites/{index.js => index.jsx} (90%) rename app/javascript/mastodon/features/filters/{added_to_filter.js => added_to_filter.jsx} (92%) rename app/javascript/mastodon/features/filters/{select_filter.js => select_filter.jsx} (93%) create mode 100644 app/javascript/mastodon/features/firehose/index.jsx delete mode 100644 app/javascript/mastodon/features/follow_recommendations/components/account.js delete mode 100644 app/javascript/mastodon/features/follow_recommendations/index.js rename app/javascript/mastodon/features/follow_requests/components/{account_authorize.js => account_authorize.jsx} (88%) rename app/javascript/mastodon/features/follow_requests/{index.js => index.jsx} (90%) rename app/javascript/mastodon/features/followed_tags/{index.js => index.jsx} (97%) rename app/javascript/mastodon/features/followers/{index.js => index.jsx} (93%) rename app/javascript/mastodon/features/following/{index.js => index.jsx} (93%) delete mode 100644 app/javascript/mastodon/features/generic_not_found/index.js rename app/javascript/mastodon/features/getting_started/components/{announcements.js => announcements.jsx} (95%) rename app/javascript/mastodon/features/getting_started/components/{trends.js => trends.jsx} (97%) rename app/javascript/mastodon/features/getting_started/{index.js => index.jsx} (96%) rename app/javascript/{flavours/glitch/features/hashtag_timeline/components/column_settings.js => mastodon/features/hashtag_timeline/components/column_settings.jsx} (96%) rename app/javascript/mastodon/features/hashtag_timeline/{index.js => index.jsx} (91%) rename app/javascript/mastodon/features/home_timeline/components/{column_settings.js => column_settings.jsx} (90%) create mode 100644 app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx rename app/javascript/mastodon/features/home_timeline/{index.js => index.jsx} (70%) rename app/javascript/mastodon/features/interaction_modal/{index.js => index.jsx} (88%) rename app/javascript/mastodon/features/keyboard_shortcuts/{index.js => index.jsx} (99%) rename app/javascript/{flavours/glitch/features/list_adder/components/account.js => mastodon/features/list_adder/components/account.jsx} (83%) rename app/javascript/mastodon/features/list_adder/components/{list.js => list.jsx} (90%) rename app/javascript/{flavours/glitch/features/list_adder/index.js => mastodon/features/list_adder/index.jsx} (95%) rename app/javascript/mastodon/features/list_editor/components/{account.js => account.jsx} (89%) rename app/javascript/mastodon/features/list_editor/components/{edit_list_form.js => edit_list_form.jsx} (86%) rename app/javascript/mastodon/features/list_editor/components/{search.js => search.jsx} (87%) rename app/javascript/mastodon/features/list_editor/{index.js => index.jsx} (94%) rename app/javascript/mastodon/features/list_timeline/{index.js => index.jsx} (75%) rename app/javascript/mastodon/features/lists/components/{new_list_form.js => new_list_form.jsx} (90%) rename app/javascript/mastodon/features/lists/{index.js => index.jsx} (94%) rename app/javascript/mastodon/features/mutes/{index.js => index.jsx} (93%) rename app/javascript/mastodon/features/notifications/components/{clear_column_button.js => clear_column_button.jsx} (61%) rename app/javascript/mastodon/features/notifications/components/{column_settings.js => column_settings.jsx} (99%) rename app/javascript/{flavours/glitch/features/notifications/components/filter_bar.js => mastodon/features/notifications/components/filter_bar.jsx} (95%) rename app/javascript/mastodon/features/notifications/components/{follow_request.js => follow_request.jsx} (85%) rename app/javascript/mastodon/features/notifications/components/{grant_permission_button.js => grant_permission_button.jsx} (71%) rename app/javascript/mastodon/features/notifications/components/{notification.js => notification.jsx} (93%) rename app/javascript/mastodon/features/notifications/components/{notifications_permission_banner.js => notifications_permission_banner.jsx} (85%) rename app/javascript/mastodon/features/notifications/components/{report.js => report.jsx} (79%) rename app/javascript/mastodon/features/notifications/components/{setting_toggle.js => setting_toggle.jsx} (90%) rename app/javascript/mastodon/features/notifications/{index.js => index.jsx} (95%) create mode 100644 app/javascript/mastodon/features/onboarding/components/arrow_small_right.jsx create mode 100644 app/javascript/mastodon/features/onboarding/components/progress_indicator.jsx create mode 100644 app/javascript/mastodon/features/onboarding/components/step.jsx create mode 100644 app/javascript/mastodon/features/onboarding/follows.jsx create mode 100644 app/javascript/mastodon/features/onboarding/index.jsx create mode 100644 app/javascript/mastodon/features/onboarding/share.jsx rename app/javascript/mastodon/features/picture_in_picture/components/{footer.js => footer.jsx} (84%) rename app/javascript/mastodon/features/picture_in_picture/components/{header.js => header.jsx} (83%) rename app/javascript/mastodon/features/picture_in_picture/{index.js => index.jsx} (93%) rename app/javascript/mastodon/features/pinned_statuses/{index.js => index.jsx} (89%) rename app/javascript/mastodon/features/privacy_policy/{index.js => index.jsx} (90%) rename app/javascript/mastodon/features/public_timeline/components/{column_settings.js => column_settings.jsx} (88%) rename app/javascript/mastodon/features/public_timeline/{index.js => index.jsx} (90%) rename app/javascript/mastodon/features/reblogs/{index.js => index.jsx} (90%) rename app/javascript/mastodon/features/report/{category.js => category.jsx} (89%) rename app/javascript/mastodon/features/report/{comment.js => comment.jsx} (92%) rename app/javascript/mastodon/features/report/components/{option.js => option.jsx} (89%) rename app/javascript/mastodon/features/report/components/{status_check_box.js => status_check_box.jsx} (86%) rename app/javascript/mastodon/features/report/{rules.js => rules.jsx} (92%) rename app/javascript/mastodon/features/report/{statuses.js => statuses.jsx} (90%) rename app/javascript/mastodon/features/report/{thanks.js => thanks.jsx} (95%) rename app/javascript/mastodon/features/standalone/compose/{index.js => index.jsx} (78%) rename app/javascript/mastodon/features/status/components/{action_bar.js => action_bar.jsx} (90%) rename app/javascript/mastodon/features/status/components/{card.js => card.jsx} (77%) rename app/javascript/mastodon/features/status/components/{detailed_status.js => detailed_status.jsx} (83%) rename app/javascript/mastodon/features/status/{index.js => index.jsx} (80%) rename app/javascript/mastodon/features/subscribed_languages_modal/{index.js => index.jsx} (96%) rename app/javascript/mastodon/features/ui/components/__tests__/{column-test.js => column-test.jsx} (97%) rename app/javascript/mastodon/features/ui/components/{actions_modal.js => actions_modal.jsx} (91%) rename app/javascript/mastodon/features/ui/components/{audio_modal.js => audio_modal.jsx} (73%) rename app/javascript/mastodon/features/ui/components/{block_modal.js => block_modal.jsx} (90%) rename app/javascript/mastodon/features/ui/components/{boost_modal.js => boost_modal.jsx} (88%) rename app/javascript/mastodon/features/ui/components/{bundle.js => bundle.jsx} (91%) rename app/javascript/mastodon/features/ui/components/{bundle_column_error.js => bundle_column_error.jsx} (95%) rename app/javascript/mastodon/features/ui/components/{bundle_modal_error.js => bundle_modal_error.jsx} (89%) rename app/javascript/mastodon/features/ui/components/{column.js => column.jsx} (93%) rename app/javascript/mastodon/features/ui/components/{column_header.js => column_header.jsx} (82%) rename app/javascript/mastodon/features/ui/components/{column_link.js => column_link.jsx} (94%) rename app/javascript/mastodon/features/ui/components/{column_loading.js => column_loading.jsx} (96%) rename app/javascript/mastodon/features/ui/components/{column_subheading.js => column_subheading.jsx} (90%) rename app/javascript/mastodon/features/ui/components/{columns_area.js => columns_area.jsx} (95%) rename app/javascript/mastodon/features/ui/components/{compare_history_modal.js => compare_history_modal.jsx} (79%) rename app/javascript/mastodon/features/ui/components/{compose_panel.js => compose_panel.jsx} (87%) rename app/javascript/mastodon/features/ui/components/{confirmation_modal.js => confirmation_modal.jsx} (91%) rename app/javascript/mastodon/features/ui/components/{disabled_account_banner.js => disabled_account_banner.jsx} (85%) rename app/javascript/{flavours/glitch/features/ui/components/drawer_loading.js => mastodon/features/ui/components/drawer_loading.jsx} (86%) rename app/javascript/mastodon/features/ui/components/{embed_modal.js => embed_modal.jsx} (93%) rename app/javascript/mastodon/features/ui/components/{filter_modal.js => filter_modal.jsx} (96%) rename app/javascript/mastodon/features/ui/components/{focal_point_modal.js => focal_point_modal.jsx} (95%) rename app/javascript/mastodon/features/ui/components/{follow_requests_column_link.js => follow_requests_column_link.jsx} (83%) delete mode 100644 app/javascript/mastodon/features/ui/components/header.js create mode 100644 app/javascript/mastodon/features/ui/components/header.jsx rename app/javascript/mastodon/features/ui/components/{image_loader.js => image_loader.jsx} (95%) rename app/javascript/{flavours/glitch/features/ui/components/image_modal.js => mastodon/features/ui/components/image_modal.jsx} (88%) rename app/javascript/mastodon/features/ui/components/{link_footer.js => link_footer.jsx} (65%) rename app/javascript/mastodon/features/ui/components/{list_panel.js => list_panel.jsx} (94%) rename app/javascript/mastodon/features/ui/components/{media_modal.js => media_modal.jsx} (90%) rename app/javascript/{flavours/glitch/features/ui/components/modal_loading.js => mastodon/features/ui/components/modal_loading.jsx} (80%) rename app/javascript/mastodon/features/ui/components/{modal_root.js => modal_root.jsx} (94%) rename app/javascript/mastodon/features/ui/components/{mute_modal.js => mute_modal.jsx} (94%) rename app/javascript/mastodon/features/ui/components/{navigation_panel.js => navigation_panel.jsx} (83%) rename app/javascript/mastodon/features/ui/components/{report_modal.js => report_modal.jsx} (97%) delete mode 100644 app/javascript/mastodon/features/ui/components/sign_in_banner.js create mode 100644 app/javascript/mastodon/features/ui/components/sign_in_banner.jsx rename app/javascript/mastodon/features/ui/components/{upload_area.js => upload_area.jsx} (90%) rename app/javascript/mastodon/features/ui/components/{video_modal.js => video_modal.jsx} (70%) rename app/javascript/{flavours/glitch/features/ui/components/zoomable_image.js => mastodon/features/ui/components/zoomable_image.jsx} (97%) rename app/javascript/mastodon/features/ui/{index.js => index.jsx} (93%) rename app/javascript/mastodon/features/ui/util/{react_router_helpers.js => react_router_helpers.jsx} (89%) rename app/javascript/{flavours/glitch/features/ui/util/reduced_motion.js => mastodon/features/ui/util/reduced_motion.jsx} (93%) rename app/javascript/mastodon/features/video/{index.js => index.jsx} (91%) rename app/javascript/mastodon/{is_mobile.js => is_mobile.ts} (55%) delete mode 100644 app/javascript/mastodon/locales/defaultMessages.json create mode 100644 app/javascript/mastodon/locales/global_locale.ts delete mode 100644 app/javascript/mastodon/locales/index.js create mode 100644 app/javascript/mastodon/locales/index.ts create mode 100644 app/javascript/mastodon/locales/intl_provider.tsx create mode 100644 app/javascript/mastodon/locales/load_locale.ts delete mode 100644 app/javascript/mastodon/locales/locale-data/co.js delete mode 100644 app/javascript/mastodon/locales/locale-data/sa.js create mode 100644 app/javascript/mastodon/locales/uz.json delete mode 100644 app/javascript/mastodon/locales/whitelist_af.json delete mode 100644 app/javascript/mastodon/locales/whitelist_an.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ar.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ast.json delete mode 100644 app/javascript/mastodon/locales/whitelist_be.json delete mode 100644 app/javascript/mastodon/locales/whitelist_bg.json delete mode 100644 app/javascript/mastodon/locales/whitelist_bn.json delete mode 100644 app/javascript/mastodon/locales/whitelist_br.json delete mode 100644 app/javascript/mastodon/locales/whitelist_bs.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ca.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ckb.json delete mode 100644 app/javascript/mastodon/locales/whitelist_co.json delete mode 100644 app/javascript/mastodon/locales/whitelist_cs.json delete mode 100644 app/javascript/mastodon/locales/whitelist_cy.json delete mode 100644 app/javascript/mastodon/locales/whitelist_da.json delete mode 100644 app/javascript/mastodon/locales/whitelist_de.json delete mode 100644 app/javascript/mastodon/locales/whitelist_el.json delete mode 100644 app/javascript/mastodon/locales/whitelist_en-GB.json delete mode 100644 app/javascript/mastodon/locales/whitelist_en.json delete mode 100644 app/javascript/mastodon/locales/whitelist_eo.json delete mode 100644 app/javascript/mastodon/locales/whitelist_es-AR.json delete mode 100644 app/javascript/mastodon/locales/whitelist_es-MX.json delete mode 100644 app/javascript/mastodon/locales/whitelist_es.json delete mode 100644 app/javascript/mastodon/locales/whitelist_et.json delete mode 100644 app/javascript/mastodon/locales/whitelist_eu.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fa.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fi.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fo.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fr-QC.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_fy.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ga.json delete mode 100644 app/javascript/mastodon/locales/whitelist_gd.json delete mode 100644 app/javascript/mastodon/locales/whitelist_gl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_he.json delete mode 100644 app/javascript/mastodon/locales/whitelist_hi.json delete mode 100644 app/javascript/mastodon/locales/whitelist_hr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_hu.json delete mode 100644 app/javascript/mastodon/locales/whitelist_hy.json delete mode 100644 app/javascript/mastodon/locales/whitelist_id.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ig.json delete mode 100644 app/javascript/mastodon/locales/whitelist_io.json delete mode 100644 app/javascript/mastodon/locales/whitelist_is.json delete mode 100644 app/javascript/mastodon/locales/whitelist_it.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ja.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ka.json delete mode 100644 app/javascript/mastodon/locales/whitelist_kab.json delete mode 100644 app/javascript/mastodon/locales/whitelist_kk.json delete mode 100644 app/javascript/mastodon/locales/whitelist_kn.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ko.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ku.json delete mode 100644 app/javascript/mastodon/locales/whitelist_kw.json delete mode 100644 app/javascript/mastodon/locales/whitelist_la.json delete mode 100644 app/javascript/mastodon/locales/whitelist_lt.json delete mode 100644 app/javascript/mastodon/locales/whitelist_lv.json delete mode 100644 app/javascript/mastodon/locales/whitelist_mk.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ml.json delete mode 100644 app/javascript/mastodon/locales/whitelist_mr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ms.json delete mode 100644 app/javascript/mastodon/locales/whitelist_my.json delete mode 100644 app/javascript/mastodon/locales/whitelist_nl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_nn.json delete mode 100644 app/javascript/mastodon/locales/whitelist_no.json delete mode 100644 app/javascript/mastodon/locales/whitelist_oc.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pa.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pt-BR.json delete mode 100644 app/javascript/mastodon/locales/whitelist_pt-PT.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ro.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ru.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sa.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sc.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sco.json delete mode 100644 app/javascript/mastodon/locales/whitelist_si.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sk.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sq.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sr-Latn.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_sv.json delete mode 100644 app/javascript/mastodon/locales/whitelist_szl.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ta.json delete mode 100644 app/javascript/mastodon/locales/whitelist_tai.json delete mode 100644 app/javascript/mastodon/locales/whitelist_te.json delete mode 100644 app/javascript/mastodon/locales/whitelist_th.json delete mode 100644 app/javascript/mastodon/locales/whitelist_tr.json delete mode 100644 app/javascript/mastodon/locales/whitelist_tt.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ug.json delete mode 100644 app/javascript/mastodon/locales/whitelist_uk.json delete mode 100644 app/javascript/mastodon/locales/whitelist_ur.json delete mode 100644 app/javascript/mastodon/locales/whitelist_vi.json delete mode 100644 app/javascript/mastodon/locales/whitelist_zgh.json delete mode 100644 app/javascript/mastodon/locales/whitelist_zh-CN.json delete mode 100644 app/javascript/mastodon/locales/whitelist_zh-HK.json delete mode 100644 app/javascript/mastodon/locales/whitelist_zh-TW.json rename app/javascript/mastodon/{main.js => main.jsx} (79%) delete mode 100644 app/javascript/mastodon/middleware/errors.js delete mode 100644 app/javascript/mastodon/middleware/loading_bar.js delete mode 100644 app/javascript/mastodon/middleware/sounds.js delete mode 100644 app/javascript/mastodon/permissions.js create mode 100644 app/javascript/mastodon/permissions.ts create mode 100644 app/javascript/mastodon/polyfills/base_polyfills.ts rename app/javascript/mastodon/{extra_polyfills.js => polyfills/extra_polyfills.ts} (75%) rename app/javascript/{flavours/glitch/load_polyfills.js => mastodon/polyfills/index.ts} (79%) create mode 100644 app/javascript/mastodon/polyfills/intl.ts rename app/javascript/mastodon/reducers/{index.js => index.ts} (69%) delete mode 100644 app/javascript/mastodon/reducers/missed_updates.js create mode 100644 app/javascript/mastodon/reducers/missed_updates.ts delete mode 100644 app/javascript/mastodon/reducers/modal.js create mode 100644 app/javascript/mastodon/reducers/modal.ts delete mode 100644 app/javascript/mastodon/scroll.js create mode 100644 app/javascript/mastodon/scroll.ts delete mode 100644 app/javascript/mastodon/store/configureStore.js create mode 100644 app/javascript/mastodon/store/index.ts create mode 100644 app/javascript/mastodon/store/middlewares/errors.ts create mode 100644 app/javascript/mastodon/store/middlewares/loading_bar.ts create mode 100644 app/javascript/mastodon/store/middlewares/sounds.ts rename app/javascript/{flavours/glitch/utils/base64.js => mastodon/utils/base64.ts} (79%) delete mode 100644 app/javascript/mastodon/utils/filters.js create mode 100644 app/javascript/mastodon/utils/filters.ts create mode 100644 app/javascript/mastodon/utils/hashtags.ts rename app/javascript/mastodon/utils/{icons.js => icons.jsx} (95%) rename app/javascript/mastodon/utils/{numbers.js => numbers.ts} (52%) delete mode 100644 app/javascript/mastodon/utils/resize_image.js delete mode 100644 app/javascript/mastodon/uuid.js create mode 100644 app/javascript/mastodon/uuid.ts rename app/javascript/packs/{admin.js => admin.jsx} (66%) rename app/javascript/packs/{public.js => public.jsx} (67%) delete mode 100644 app/javascript/packs/share.js create mode 100644 app/javascript/packs/share.jsx create mode 100644 app/javascript/packs/sign_up.js create mode 100644 app/javascript/styles/mastodon/rich_text.scss create mode 100644 app/javascript/types/image.d.ts create mode 100644 app/javascript/types/resources.ts create mode 100644 app/javascript/types/util.ts create mode 100644 app/lib/admin/metrics/dimension/query_helper.rb create mode 100644 app/lib/admin/metrics/measure/query_helper.rb create mode 100644 app/lib/admin/system_check/media_privacy_check.rb create mode 100644 app/lib/attachment_batch.rb delete mode 100644 app/lib/hash_object.rb delete mode 100644 app/lib/settings/extend.rb delete mode 100644 app/lib/toc_generator.rb delete mode 100644 app/lib/user_settings_decorator.rb create mode 100644 app/lib/user_settings_serializer.rb create mode 100644 app/lib/vacuum/imports_vacuum.rb create mode 100644 app/lib/webhooks/payload_renderer.rb create mode 100644 app/models/bulk_import.rb create mode 100644 app/models/bulk_import_row.rb create mode 100644 app/models/concerns/account_search.rb create mode 100644 app/models/concerns/has_user_settings.rb create mode 100644 app/models/concerns/status_safe_reblog_insert.rb create mode 100644 app/models/form/import.rb create mode 100644 app/models/translation.rb create mode 100644 app/models/user_settings.rb create mode 100644 app/models/user_settings/dsl.rb create mode 100644 app/models/user_settings/glue.rb create mode 100644 app/models/user_settings/namespace.rb create mode 100644 app/models/user_settings/setting.rb create mode 100644 app/serializers/rest/admin/trends/link_serializer.rb create mode 100644 app/serializers/rest/admin/trends/links/preview_card_provider_serializer.rb create mode 100644 app/serializers/rest/admin/trends/status_serializer.rb create mode 100644 app/services/bulk_import_row_service.rb create mode 100644 app/services/bulk_import_service.rb create mode 100644 app/services/follow_migration_service.rb create mode 100644 app/services/remove_domains_from_followers_service.rb delete mode 100644 app/validators/html_validator.rb delete mode 100644 app/validators/import_validator.rb create mode 100644 app/views/admin/domain_blocks/confirm_suspension.html.haml delete mode 100644 app/views/application/_sidebar.html.haml create mode 100644 app/views/auth/shared/_progress.html.haml create mode 100644 app/views/kaminari/_gap.html.haml create mode 100644 app/views/mail_subscriptions/create.html.haml create mode 100644 app/views/mail_subscriptions/show.html.haml create mode 100644 app/views/settings/imports/index.html.haml delete mode 100644 app/views/settings/shared/_links.html.haml create mode 100644 app/views/settings/shared/_profile_navigation.html.haml create mode 100644 app/views/settings/verifications/show.html.haml create mode 100644 app/workers/activitypub/migrated_follow_delivery_worker.rb create mode 100644 app/workers/bulk_import_worker.rb create mode 100644 app/workers/import/row_worker.rb create mode 100644 config/formatjs-formatter.js create mode 100644 config/initializers/redis.rb delete mode 100644 config/initializers/statsd.rb create mode 100644 config/locales/activerecord.uz.yml delete mode 100644 config/locales/activerecord.zh_Hant.yml create mode 100644 config/locales/devise.uz.yml create mode 100644 config/locales/doorkeeper.uz.yml delete mode 100644 config/locales/en_GB.yml delete mode 100644 config/locales/simple_form.en_GB.yml create mode 100644 config/locales/simple_form.uz.yml delete mode 100644 config/locales/tai.yml create mode 100644 config/locales/uz.yml create mode 100644 config/routes/admin.rb create mode 100644 config/routes/api.rb create mode 100644 config/routes/settings.rb delete mode 100644 config/webpack/generateLocalePacks.js delete mode 100644 config/webpack/translationRunner.js create mode 100644 db/migrate/20230129023109_add_template_to_webhooks.rb create mode 100644 db/migrate/20230215074327_add_settings_to_users.rb create mode 100644 db/migrate/20230215074423_move_user_settings.rb create mode 100644 db/migrate/20230215074424_move_glitch_user_settings.rb create mode 100644 db/migrate/20230330135507_create_bulk_imports.rb create mode 100644 db/migrate/20230330140036_create_bulk_import_rows.rb create mode 100644 db/migrate/20230330155710_add_follow_request_id_to_list_accounts.rb create mode 100644 db/migrate/20230524190515_add_index_accounts_on_domain_and_id.rb create mode 100644 db/migrate/20230524192812_fix_account_domain_casing.rb create mode 100644 db/migrate/20230524194155_add_index_instances_on_reverse_domain.rb create mode 100644 db/migrate/20230531153942_add_primary_key_to_accounts_tags_join_table.rb create mode 100644 db/migrate/20230531154811_add_primary_key_to_statuses_tags_join_table.rb create mode 100644 db/migrate/20230605085710_add_exclusive_to_lists.rb create mode 100644 db/migrate/20230605085711_add_time_zone_to_users.rb create mode 100644 db/migrate/20230630145300_add_index_backups_on_user_id.rb create mode 100644 db/migrate/20230702131023_add_superapp_index_to_applications.rb create mode 100644 db/migrate/20230702151753_add_index_user_on_unconfirmed_email.rb create mode 100644 lib/action_controller/conditional_get_extensions.rb create mode 100644 lib/chewy/strategy/bypass_with_warning.rb create mode 100644 lib/generators/post_deployment_migration/USAGE create mode 100644 lib/generators/post_deployment_migration/post_deployment_migration_generator.rb create mode 100644 lib/generators/post_deployment_migration/templates/migration.erb delete mode 100644 lib/generators/post_deployment_migration_generator.rb create mode 100644 lib/linter/haml_middle_dot.rb create mode 100644 lib/linter/rubocop_middle_dot.rb delete mode 100644 lib/mastodon/cache_cli.rb rename lib/mastodon/{accounts_cli.rb => cli/accounts.rb} (88%) create mode 100644 lib/mastodon/cli/base.rb create mode 100644 lib/mastodon/cli/cache.rb rename lib/mastodon/{canonical_email_blocks_cli.rb => cli/canonical_email_blocks.rb} (83%) rename lib/mastodon/{domains_cli.rb => cli/domains.rb} (93%) rename lib/mastodon/{email_domain_blocks_cli.rb => cli/email_domain_blocks.rb} (90%) rename lib/mastodon/{emoji_cli.rb => cli/emoji.rb} (92%) rename lib/mastodon/{feeds_cli.rb => cli/feeds.rb} (63%) rename lib/mastodon/{ip_blocks_cli.rb => cli/ip_blocks.rb} (80%) rename lib/{cli.rb => mastodon/cli/main.rb} (67%) rename lib/mastodon/{maintenance_cli.rb => cli/maintenance.rb} (77%) rename lib/mastodon/{media_cli.rb => cli/media.rb} (87%) rename lib/mastodon/{preview_cards_cli.rb => cli/preview_cards.rb} (80%) rename lib/mastodon/{cli_helper.rb => cli/progress_helper.rb} (58%) rename lib/mastodon/{search_cli.rb => cli/search.rb} (80%) rename lib/mastodon/{settings_cli.rb => cli/settings.rb} (76%) rename lib/mastodon/{statuses_cli.rb => cli/statuses.rb} (96%) rename lib/mastodon/{upgrade_cli.rb => cli/upgrade.rb} (86%) create mode 100644 lib/mastodon/migration_warning.rb create mode 100644 lib/public_file_server_middleware.rb delete mode 100644 lib/templates/rails/post_deployment_migration/migration.rb create mode 100644 spec/chewy/accounts_index_spec.rb create mode 100644 spec/chewy/statuses_index_spec.rb create mode 100644 spec/chewy/tags_index_spec.rb create mode 100644 spec/controllers/.rubocop.yml create mode 100644 spec/controllers/activitypub/claims_controller_spec.rb create mode 100644 spec/controllers/admin/account_actions_controller_spec.rb create mode 100644 spec/controllers/admin/announcements_controller_spec.rb rename spec/controllers/admin/{change_email_controller_spec.rb => change_emails_controller_spec.rb} (68%) create mode 100644 spec/controllers/admin/follow_recommendations_controller_spec.rb create mode 100644 spec/controllers/admin/ip_blocks_controller_spec.rb create mode 100644 spec/controllers/admin/relationships_controller_spec.rb create mode 100644 spec/controllers/admin/relays_controller_spec.rb create mode 100644 spec/controllers/admin/rules_controller_spec.rb create mode 100644 spec/controllers/admin/settings/about_controller_spec.rb create mode 100644 spec/controllers/admin/settings/appearance_controller_spec.rb create mode 100644 spec/controllers/admin/settings/content_retention_controller_spec.rb create mode 100644 spec/controllers/admin/settings/discovery_controller_spec.rb create mode 100644 spec/controllers/admin/settings/registrations_controller_spec.rb create mode 100644 spec/controllers/admin/site_uploads_controller_spec.rb create mode 100644 spec/controllers/admin/trends/links/preview_card_providers_controller_spec.rb create mode 100644 spec/controllers/admin/trends/links_controller_spec.rb create mode 100644 spec/controllers/admin/trends/statuses_controller_spec.rb create mode 100644 spec/controllers/admin/trends/tags_controller_spec.rb rename spec/controllers/admin/users/{roles_controller.rb => roles_controller_spec.rb} (92%) create mode 100644 spec/controllers/admin/warning_presets_controller_spec.rb create mode 100644 spec/controllers/admin/webhooks/secrets_controller_spec.rb create mode 100644 spec/controllers/admin/webhooks_controller_spec.rb create mode 100644 spec/controllers/api/v1/accounts/familiar_followers_controller_spec.rb create mode 100644 spec/controllers/api/v1/accounts/featured_tags_controller_spec.rb create mode 100644 spec/controllers/api/v1/accounts/identity_proofs_controller_spec.rb create mode 100644 spec/controllers/api/v1/accounts/lookup_controller_spec.rb delete mode 100644 spec/controllers/api/v1/admin/account_actions_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/dimensions_controller_spec.rb delete mode 100644 spec/controllers/api/v1/admin/domain_allows_controller_spec.rb delete mode 100644 spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/measures_controller_spec.rb delete mode 100644 spec/controllers/api/v1/admin/reports_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/retention_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/trends/links/preview_card_providers_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/trends/links_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/trends/statuses_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/trends/tags_controller_spec.rb delete mode 100644 spec/controllers/api/v1/apps/credentials_controller_spec.rb create mode 100644 spec/controllers/api/v1/directories_controller_spec.rb delete mode 100644 spec/controllers/api/v1/domain_blocks_controller_spec.rb create mode 100644 spec/controllers/api/v1/featured_tags/suggestions_controller_spec.rb delete mode 100644 spec/controllers/api/v1/follow_requests_controller_spec.rb create mode 100644 spec/controllers/api/v1/instances/domain_blocks_controller_spec.rb create mode 100644 spec/controllers/api/v1/instances/extended_descriptions_controller_spec.rb create mode 100644 spec/controllers/api/v1/instances/privacy_policies_controller_spec.rb create mode 100644 spec/controllers/api/v1/instances/rules_controller_spec.rb create mode 100644 spec/controllers/api/v1/instances/translation_languages_controller_spec.rb delete mode 100644 spec/controllers/api/v1/lists_controller_spec.rb create mode 100644 spec/controllers/api/v1/preferences_controller_spec.rb create mode 100644 spec/controllers/api/v1/scheduled_statuses_controller_spec.rb create mode 100644 spec/controllers/api/v1/statuses/translations_controller_spec.rb delete mode 100644 spec/controllers/api/v1/suggestions_controller_spec.rb delete mode 100644 spec/controllers/api/v1/tags_controller_spec.rb create mode 100644 spec/controllers/api/v1/trends/links_controller_spec.rb create mode 100644 spec/controllers/api/v1/trends/statuses_controller_spec.rb create mode 100644 spec/controllers/api/v2/instances_controller_spec.rb create mode 100644 spec/controllers/api/v2/suggestions_controller_spec.rb create mode 100644 spec/controllers/auth/setup_controller_spec.rb create mode 100644 spec/controllers/custom_css_controller_spec.rb create mode 100644 spec/controllers/filters/statuses_controller_spec.rb create mode 100644 spec/controllers/filters_controller_spec.rb create mode 100644 spec/controllers/privacy_controller_spec.rb create mode 100644 spec/controllers/settings/aliases_controller_spec.rb create mode 100644 spec/controllers/settings/exports/blocked_domains_controller_spec.rb create mode 100644 spec/controllers/settings/exports/lists_controller_spec.rb create mode 100644 spec/controllers/settings/login_activities_controller_spec.rb create mode 100644 spec/controllers/settings/migration/redirects_controller_spec.rb create mode 100644 spec/controllers/settings/pictures_controller_spec.rb create mode 100644 spec/controllers/settings/preferences/appearance_controller_spec.rb create mode 100644 spec/controllers/settings/preferences/base_controller_spec.rb delete mode 100644 spec/fabricators/account_alias_fabricator.rb delete mode 100644 spec/fabricators/account_deletion_request_fabricator.rb delete mode 100644 spec/fabricators/account_tag_stat_fabricator.rb delete mode 100644 spec/fabricators/announcement_mute_fabricator.rb delete mode 100644 spec/fabricators/announcement_reaction_fabricator.rb create mode 100644 spec/fabricators/bulk_import_fabricator.rb create mode 100644 spec/fabricators/bulk_import_row_fabricator.rb delete mode 100644 spec/fabricators/conversation_account_fabricator.rb delete mode 100644 spec/fabricators/conversation_mute_fabricator.rb delete mode 100644 spec/fabricators/custom_emoji_category_fabricator.rb delete mode 100644 spec/fabricators/follow_recommendation_suppression_fabricator.rb delete mode 100644 spec/fabricators/import_fabricator.rb delete mode 100644 spec/fabricators/ip_block_fabricator.rb create mode 100644 spec/fabricators/preview_card_provider_fabricator.rb delete mode 100644 spec/fabricators/status_edit_fabricator.rb delete mode 100644 spec/fabricators/user_invite_request_fabricator.rb delete mode 100644 spec/fabricators/web_setting_fabricator.rb create mode 100644 spec/fabricators_spec.rb create mode 100644 spec/features/admin/domain_blocks_spec.rb create mode 100644 spec/features/captcha_spec.rb create mode 100644 spec/features/oauth_spec.rb rename app/javascript/flavours/glitch/styles/components/metadata.scss => spec/fixtures/files/empty.csv (100%) create mode 100644 spec/fixtures/files/following_accounts.csv create mode 100644 spec/fixtures/files/lists.csv create mode 100644 spec/fixtures/files/muted_accounts.csv create mode 100644 spec/generators/post_deployment_migration_generator_spec.rb rename spec/helpers/admin/{action_log_helper_spec.rb => action_logs_helper_spec.rb} (50%) create mode 100644 spec/helpers/admin/dashboard_helper_spec.rb create mode 100644 spec/helpers/admin/trends/statuses_helper_spec.rb create mode 100644 spec/helpers/media_component_helper_spec.rb create mode 100644 spec/helpers/react_component_helper_spec.rb create mode 100644 spec/helpers/settings_helper_spec.rb create mode 100644 spec/lib/account_reach_finder_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/instance_accounts_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/instance_languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/servers_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/software_versions_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/sources_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/space_usage_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/tag_languages_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/dimension/tag_servers_dimension_spec.rb create mode 100644 spec/lib/admin/metrics/measure/active_users_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_accounts_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_followers_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_follows_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_media_attachments_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_reports_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/instance_statuses_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/interactions_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/new_users_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/opened_reports_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/resolved_reports_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_accounts_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_servers_measure_spec.rb create mode 100644 spec/lib/admin/metrics/measure/tag_uses_measure_spec.rb create mode 100644 spec/lib/admin/system_check/base_check_spec.rb create mode 100644 spec/lib/admin/system_check/database_schema_check_spec.rb create mode 100644 spec/lib/admin/system_check/elasticsearch_check_spec.rb create mode 100644 spec/lib/admin/system_check/media_privacy_check_spec.rb create mode 100644 spec/lib/admin/system_check/message_spec.rb create mode 100644 spec/lib/admin/system_check/rules_check_spec.rb create mode 100644 spec/lib/admin/system_check/sidekiq_process_check_spec.rb create mode 100644 spec/lib/admin/system_check_spec.rb delete mode 100644 spec/lib/hash_object_spec.rb create mode 100644 spec/lib/importer/accounts_index_importer_spec.rb create mode 100644 spec/lib/importer/base_importer_spec.rb create mode 100644 spec/lib/importer/statuses_index_importer_spec.rb create mode 100644 spec/lib/importer/tags_index_importer_spec.rb create mode 100644 spec/lib/mastodon/cli/accounts_spec.rb create mode 100644 spec/lib/mastodon/cli/cache_spec.rb create mode 100644 spec/lib/mastodon/cli/canonical_email_blocks_spec.rb create mode 100644 spec/lib/mastodon/cli/domains_spec.rb create mode 100644 spec/lib/mastodon/cli/email_domain_blocks_spec.rb create mode 100644 spec/lib/mastodon/cli/emoji_spec.rb create mode 100644 spec/lib/mastodon/cli/feeds_spec.rb create mode 100644 spec/lib/mastodon/cli/ip_blocks_spec.rb create mode 100644 spec/lib/mastodon/cli/main_spec.rb create mode 100644 spec/lib/mastodon/cli/maintenance_spec.rb create mode 100644 spec/lib/mastodon/cli/media_spec.rb create mode 100644 spec/lib/mastodon/cli/preview_cards_spec.rb create mode 100644 spec/lib/mastodon/cli/search_spec.rb create mode 100644 spec/lib/mastodon/cli/settings_spec.rb create mode 100644 spec/lib/mastodon/cli/statuses_spec.rb create mode 100644 spec/lib/mastodon/cli/upgrade_spec.rb create mode 100644 spec/lib/mastodon/migration_warning_spec.rb create mode 100644 spec/lib/search_query_transformer_spec.rb delete mode 100644 spec/lib/settings/extend_spec.rb delete mode 100644 spec/lib/settings/scoped_settings_spec.rb create mode 100644 spec/lib/translation_service/deepl_spec.rb create mode 100644 spec/lib/translation_service/libre_translate_spec.rb delete mode 100644 spec/lib/user_settings_decorator_spec.rb create mode 100644 spec/lib/vacuum/imports_vacuum_spec.rb create mode 100644 spec/lib/webhooks/payload_renderer_spec.rb create mode 100644 spec/locales/i18n_spec.rb create mode 100644 spec/models/account_warning_preset_spec.rb create mode 100644 spec/models/admin/appeal_filter_spec.rb create mode 100644 spec/models/extended_description_spec.rb create mode 100644 spec/models/form/account_batch_spec.rb create mode 100644 spec/models/form/admin_settings_spec.rb create mode 100644 spec/models/form/import_spec.rb create mode 100644 spec/models/form/status_filter_batch_action_spec.rb create mode 100644 spec/models/preview_card_provider_spec.rb create mode 100644 spec/models/privacy_policy_spec.rb create mode 100644 spec/models/user_settings/namespace_spec.rb create mode 100644 spec/models/user_settings/setting_spec.rb create mode 100644 spec/models/user_settings_spec.rb create mode 100644 spec/policies/account_warning_preset_policy_spec.rb create mode 100644 spec/policies/admin/status_policy_spec.rb create mode 100644 spec/policies/announcement_policy_spec.rb create mode 100644 spec/policies/appeal_policy_spec.rb create mode 100644 spec/policies/canonical_email_block_policy_spec.rb create mode 100644 spec/policies/delivery_policy_spec.rb create mode 100644 spec/policies/follow_recommendation_policy_spec.rb create mode 100644 spec/policies/ip_block_policy_spec.rb create mode 100644 spec/policies/preview_card_policy_spec.rb create mode 100644 spec/policies/preview_card_provider_policy_spec.rb create mode 100644 spec/policies/rule_policy_spec.rb create mode 100644 spec/policies/webhook_policy_spec.rb create mode 100644 spec/requests/anonymous_cookies_spec.rb create mode 100644 spec/requests/api/v1/accounts_show_spec.rb create mode 100644 spec/requests/api/v1/admin/account_actions_spec.rb create mode 100644 spec/requests/api/v1/admin/canonical_email_blocks_spec.rb create mode 100644 spec/requests/api/v1/admin/domain_allows_spec.rb create mode 100644 spec/requests/api/v1/admin/domain_blocks_spec.rb create mode 100644 spec/requests/api/v1/admin/email_domain_blocks_spec.rb create mode 100644 spec/requests/api/v1/admin/ip_blocks_spec.rb create mode 100644 spec/requests/api/v1/admin/reports_spec.rb create mode 100644 spec/requests/api/v1/apps/credentials_spec.rb rename spec/{controllers/api/v1/apps_controller_spec.rb => requests/api/v1/apps_spec.rb} (63%) create mode 100644 spec/requests/api/v1/domain_blocks_spec.rb create mode 100644 spec/requests/api/v1/featured_tags_spec.rb create mode 100644 spec/requests/api/v1/follow_requests_spec.rb create mode 100644 spec/requests/api/v1/lists_spec.rb create mode 100644 spec/requests/api/v1/suggestions_spec.rb create mode 100644 spec/requests/api/v1/tags_spec.rb create mode 100644 spec/requests/backups_spec.rb create mode 100644 spec/requests/cache_spec.rb create mode 100644 spec/requests/follower_accounts_spec.rb create mode 100644 spec/requests/following_accounts_spec.rb create mode 100644 spec/serializers/activitypub/device_serializer_spec.rb rename spec/serializers/activitypub/{note_spec.rb => note_serializer_spec.rb} (53%) create mode 100644 spec/serializers/activitypub/one_time_key_serializer_spec.rb create mode 100644 spec/serializers/activitypub/undo_like_serializer_spec.rb rename spec/serializers/activitypub/{update_poll_spec.rb => update_poll_serializer_spec.rb} (88%) create mode 100644 spec/serializers/activitypub/vote_serializer_spec.rb create mode 100644 spec/serializers/rest/account_serializer_spec.rb create mode 100644 spec/serializers/rest/encrypted_message_serializer_spec.rb create mode 100644 spec/serializers/rest/instance_serializer_spec.rb create mode 100644 spec/serializers/rest/keys/claim_result_serializer_spec.rb create mode 100644 spec/serializers/rest/keys/device_serializer_spec.rb create mode 100644 spec/serializers/rest/keys/query_result_serializer_spec.rb create mode 100644 spec/serializers/rest/suggestion_serializer_spec.rb create mode 100644 spec/services/backup_service_spec.rb create mode 100644 spec/services/bulk_import_row_service_spec.rb create mode 100644 spec/services/bulk_import_service_spec.rb rename spec/services/{remove_from_follwers_service_spec.rb => remove_from_followers_service_spec.rb} (94%) create mode 100644 spec/services/translate_status_service_spec.rb create mode 100644 spec/support/examples/api.rb create mode 100644 spec/support/examples/lib/admin/checks.rb delete mode 100644 spec/support/examples/lib/settings/scoped_settings.rb delete mode 100644 spec/support/examples/lib/settings/settings_extended.rb create mode 100644 spec/workers/admin/account_deletion_worker_spec.rb create mode 100644 spec/workers/bulk_import_worker_spec.rb create mode 100644 spec/workers/cache_buster_worker_spec.rb create mode 100644 spec/workers/import/row_worker_spec.rb create mode 100644 spec/workers/poll_expiration_notify_worker_spec.rb create mode 100644 spec/workers/post_process_media_worker_spec.rb create mode 100644 spec/workers/push_conversation_worker_spec.rb create mode 100644 spec/workers/push_encrypted_message_worker_spec.rb create mode 100644 spec/workers/push_update_worker_spec.rb create mode 100644 spec/workers/redownload_avatar_worker_spec.rb create mode 100644 spec/workers/redownload_header_worker_spec.rb create mode 100644 spec/workers/remove_featured_tag_worker_spec.rb create mode 100644 spec/workers/resolve_account_worker_spec.rb create mode 100644 spec/workers/scheduler/follow_recommendations_scheduler_spec.rb create mode 100644 spec/workers/scheduler/indexing_scheduler_spec.rb create mode 100644 spec/workers/scheduler/instance_refresh_scheduler_spec.rb create mode 100644 spec/workers/scheduler/ip_cleanup_scheduler_spec.rb create mode 100644 spec/workers/scheduler/pghero_scheduler_spec.rb create mode 100644 spec/workers/scheduler/scheduled_statuses_scheduler_spec.rb create mode 100644 spec/workers/scheduler/suspended_user_cleanup_scheduler_spec.rb create mode 100644 spec/workers/scheduler/trends/refresh_scheduler_spec.rb create mode 100644 spec/workers/scheduler/trends/review_notifications_scheduler_spec.rb create mode 100644 spec/workers/scheduler/user_cleanup_scheduler_spec.rb create mode 100644 spec/workers/scheduler/vacuum_scheduler_spec.rb create mode 100644 spec/workers/unpublish_announcement_worker_spec.rb create mode 100644 spec/workers/verify_account_links_worker_spec.rb create mode 100644 spec/workers/webhooks/delivery_worker_spec.rb create mode 100644 tsconfig.json diff --git a/.bundler-audit.yml b/.bundler-audit.yml new file mode 100644 index 0000000000..f84ec80872 --- /dev/null +++ b/.bundler-audit.yml @@ -0,0 +1,3 @@ +--- +ignore: + - CVE-2015-9284 # Mitigation following https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284#mitigating-in-rails-applications diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index a373d685e0..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,225 +0,0 @@ -version: 2.1 - -orbs: - ruby: circleci/ruby@2.0.0 - node: circleci/node@5.0.3 - -executors: - default: - parameters: - ruby-version: - type: string - docker: - - image: cimg/ruby:<< parameters.ruby-version >> - environment: - BUNDLE_JOBS: 3 - BUNDLE_RETRY: 3 - CONTINUOUS_INTEGRATION: true - DB_HOST: localhost - DB_USER: root - DISABLE_SIMPLECOV: true - RAILS_ENV: test - - image: cimg/postgres:14.5 - environment: - POSTGRES_USER: root - POSTGRES_HOST_AUTH_METHOD: trust - - image: cimg/redis:7.0 - -commands: - install-system-dependencies: - steps: - - run: - name: Install system dependencies - command: | - sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev - install-ruby-dependencies: - parameters: - ruby-version: - type: string - steps: - - run: - command: | - bundle config clean 'true' - bundle config frozen 'true' - bundle config without 'development production' - name: Set bundler settings - - ruby/install-deps: - bundler-version: '2.3.26' - key: ruby<< parameters.ruby-version >>-gems-v1 - wait-db: - steps: - - run: - command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m - name: Wait for PostgreSQL and Redis - -jobs: - build: - docker: - - image: cimg/ruby:3.0-node - environment: - RAILS_ENV: test - steps: - - checkout - - install-system-dependencies - - install-ruby-dependencies: - ruby-version: '3.0' - - node/install-packages: - cache-version: v1 - pkg-manager: yarn - - run: - command: | - export NODE_OPTIONS=--openssl-legacy-provider - ./bin/rails assets:precompile - name: Precompile assets - - persist_to_workspace: - paths: - - public/assets - - public/packs-test - root: . - - test: - parameters: - ruby-version: - type: string - executor: - name: default - ruby-version: << parameters.ruby-version >> - environment: - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - parallelism: 4 - steps: - - checkout - - install-system-dependencies - - run: - command: sudo apt-get install -y ffmpeg imagemagick libpam-dev - name: Install additional system dependencies - - run: - command: bundle config with 'pam_authentication' - name: Enable PAM authentication - - install-ruby-dependencies: - ruby-version: << parameters.ruby-version >> - - attach_workspace: - at: . - - wait-db - - run: - command: ./bin/rails db:create db:schema:load db:seed - name: Load database schema - - ruby/rspec-test - - test-migrations: - executor: - name: default - ruby-version: '3.0' - steps: - - checkout - - install-system-dependencies - - install-ruby-dependencies: - ruby-version: '3.0' - - wait-db - - run: - command: ./bin/rails db:create - name: Create database - - run: - command: ./bin/rails db:migrate VERSION=20171010025614 - name: Run migrations up to v2.0.0 - - run: - command: ./bin/rails tests:migrations:populate_v2 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate VERSION=20180514140000 - name: Run migrations up to v2.4.0 - - run: - command: ./bin/rails tests:migrations:populate_v2_4 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate VERSION=20180707154237 - name: Run migrations up to v2.4.3 - - run: - command: ./bin/rails tests:migrations:populate_v2_4_3 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate - name: Run all remaining migrations - - run: - command: ./bin/rails tests:migrations:check_database - name: Check migration result - - test-two-step-migrations: - executor: - name: default - ruby-version: '3.0' - steps: - - checkout - - install-system-dependencies - - install-ruby-dependencies: - ruby-version: '3.0' - - wait-db - - run: - command: ./bin/rails db:create - name: Create database - - run: - command: ./bin/rails db:migrate VERSION=20171010025614 - name: Run migrations up to v2.0.0 - - run: - command: ./bin/rails tests:migrations:populate_v2 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate VERSION=20180514140000 - name: Run pre-deployment migrations up to v2.4.0 - environment: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - run: - command: ./bin/rails tests:migrations:populate_v2_4 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate VERSION=20180707154237 - name: Run migrations up to v2.4.3 - environment: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - run: - command: ./bin/rails tests:migrations:populate_v2_4_3 - name: Populate database with test data - - run: - command: ./bin/rails db:migrate - name: Run all remaining pre-deployment migrations - environment: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - run: - command: ./bin/rails db:migrate - name: Run all post-deployment migrations - - run: - command: ./bin/rails tests:migrations:check_database - name: Check migration result - -workflows: - version: 2 - build-and-test: - jobs: - - build - - test: - matrix: - parameters: - ruby-version: - - '2.7' - - '3.0' - name: test-ruby<< matrix.ruby-version >> - requires: - - build - - test-migrations: - requires: - - build - - test-two-step-migrations: - requires: - - build - - node/run: - cache-version: v1 - name: test-webui - pkg-manager: yarn - requires: - - build - version: '16.18' - yarn-run: test:jest diff --git a/.codeclimate.yml b/.codeclimate.yml deleted file mode 100644 index 59051aae7a..0000000000 --- a/.codeclimate.yml +++ /dev/null @@ -1,39 +0,0 @@ -version: '2' -checks: - argument-count: - enabled: false - complex-logic: - enabled: false - file-lines: - enabled: false - method-complexity: - enabled: false - method-count: - enabled: false - method-lines: - enabled: false - nested-control-flow: - enabled: false - return-statements: - enabled: false - similar-code: - enabled: false - identical-code: - enabled: false -plugins: - brakeman: - enabled: true - bundler-audit: - enabled: true - eslint: - enabled: false - rubocop: - enabled: false - sass-lint: - enabled: false -exclude_patterns: - - spec/ - - vendor/asset/ - - - app/javascript/mastodon/locales/**/*.json - - config/locales/**/*.yml diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index bcd3104124..0000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,23 +0,0 @@ -version = 1 - -test_patterns = ["app/javascript/mastodon/**/__tests__/**"] - -exclude_patterns = [ - "db/migrate/**", - "db/post_migrate/**" -] - -[[analyzers]] -name = "ruby" -enabled = true - -[[analyzers]] -name = "javascript" -enabled = true - - [analyzers.meta] - environment = [ - "browser", - "jest", - "nodejs" - ] diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 425b86a6bb..f991036add 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,16 +1,14 @@ -# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster -ARG VARIANT=3.1-bullseye -FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT} +# For details, see https://github.com/devcontainers/images/tree/main/src/ruby +FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye # Install Rails # RUN gem install rails webdrivers # Default value to allow debug server to serve content over GitHub Codespace's port forwarding service # The value is a comma-separated list of allowed domains -ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev" +ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev" -# [Choice] Node.js version: lts/*, 18, 16, 14 -ARG NODE_VERSION="lts/*" +ARG NODE_VERSION="16" RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" # [Optional] Uncomment this section to install additional OS packages. @@ -22,3 +20,5 @@ RUN gem install foreman # [Optional] Uncomment this line to install global node packages. RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1 + +COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b98f6a21e0..17208a84e9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,24 +1,13 @@ +// For more details, see https://aka.ms/devcontainer.json. { "name": "Mastodon", "dockerComposeFile": "docker-compose.yml", "service": "app", - "workspaceFolder": "/mastodon", - - // Set *default* container specific settings.json values on container create. - "settings": {}, - - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "EditorConfig.EditorConfig", - "dbaeumer.vscode-eslint", - "rebornix.Ruby", - "webben.browserslist" - ], + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // Features to add to the dev container. More info: https://containers.dev/features. "features": { - "ghcr.io/devcontainers/features/sshd:1": { - "version": "latest" - } + "ghcr.io/devcontainers/features/sshd:1": {} }, // Use 'forwardPorts' to make a list of ports inside the container available locally. @@ -26,8 +15,18 @@ "forwardPorts": [3000, 4000], // Use 'postCreateCommand' to run commands after the container is created. + "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", "postCreateCommand": ".devcontainer/post-create.sh", + "waitFor": "postCreateCommand", - // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": {}, + // Add the IDs of extensions you want installed when the container is created. + "extensions": ["EditorConfig.EditorConfig", "webben.browserslist"] + } + } } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 95f401379c..a2658ea8ba 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,19 +5,12 @@ services: build: context: . dockerfile: Dockerfile - args: - # Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6 - # Append -bullseye or -buster to pin to an OS version. - # Use -bullseye variants on local arm64/Apple Silicon. - VARIANT: '3.0-bullseye' - # Optional Node.js version to install - NODE_VERSION: '16' volumes: - - ..:/mastodon:cached + - ../..:/workspaces:cached environment: RAILS_ENV: development NODE_ENV: development - + BIND: 0.0.0.0 REDIS_HOST: redis REDIS_PORT: '6379' DB_HOST: db @@ -30,10 +23,12 @@ services: LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000 # Overrides default command so things don't shut down after the process ends. command: sleep infinity + ports: + - '127.0.0.1:3000:3000' + - '127.0.0.1:4000:4000' networks: - external_network - internal_network - user: vscode db: image: postgres:14-alpine @@ -49,7 +44,7 @@ services: - internal_network redis: - image: redis:6-alpine + image: redis:7-alpine restart: unless-stopped volumes: - redis-data:/data @@ -74,15 +69,19 @@ services: hard: -1 libretranslate: - image: libretranslate/libretranslate:v1.2.9 + image: libretranslate/libretranslate:v1.3.11 restart: unless-stopped + volumes: + - lt-data:/home/libretranslate/.local networks: + - external_network - internal_network volumes: postgres-data: redis-data: es-data: + lt-data: networks: external_network: diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 02f488f120..a075cc7b3b 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -3,17 +3,22 @@ set -e # Fail the whole script on first error # Fetch Ruby gem dependencies -bundle install --path vendor/bundle --with='development test' - -# Fetch Javascript dependencies -yarn install +bundle config path 'vendor/bundle' +bundle config with 'development test' +bundle install # Make Gemfile.lock pristine again git checkout -- Gemfile.lock +# Fetch Javascript dependencies +yarn --frozen-lockfile + # [re]create, migrate, and seed the test database RAILS_ENV=test ./bin/rails db:setup +# [re]create, migrate, and seed the development database +RAILS_ENV=development ./bin/rails db:setup + # Precompile assets for development RAILS_ENV=development ./bin/rails assets:precompile diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt new file mode 100644 index 0000000000..488cf92857 --- /dev/null +++ b/.devcontainer/welcome-message.txt @@ -0,0 +1,8 @@ +👋 Welcome to "Mastodon" in GitHub Codespaces! + +🛠️ Your environment is fully setup with all the required software. + +🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1). + +📝 Edit away, run your app as usual, and we'll automatically make it available for you to access. + diff --git a/.editorconfig b/.editorconfig index 5f8702cf89..b5217da4af 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,4 @@ insert_final_newline = true charset = utf-8 indent_style = space indent_size = 2 +trim_trailing_whitespace = true diff --git a/.eslintrc.js b/.eslintrc.js index 03af2975b8..206faa1c7a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,70 +3,66 @@ module.exports = { extends: [ 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/recommended', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended', + 'plugin:prettier/recommended', ], env: { browser: true, node: true, es6: true, - jest: true, }, globals: { ATTACHMENT_HOST: false, }, - parser: '@babel/eslint-parser', + parser: '@typescript-eslint/parser', plugins: [ 'react', 'jsx-a11y', 'import', 'promise', + '@typescript-eslint', + 'formatjs', ], parserOptions: { sourceType: 'module', ecmaFeatures: { - experimentalObjectRestSpread: true, jsx: true, }, ecmaVersion: 2021, + requireConfigFile: false, + babelOptions: { + configFile: false, + presets: ['@babel/react', '@babel/env'], + }, }, settings: { react: { version: 'detect', }, - 'import/extensions': [ - '.js', - ], 'import/ignore': [ 'node_modules', '\\.(css|scss|json)$', ], 'import/resolver': { - node: { - paths: ['app/javascript'], - }, + typescript: {}, }, }, rules: { - 'brace-style': 'warn', - 'comma-dangle': ['error', 'always-multiline'], - 'comma-spacing': [ - 'warn', - { - before: false, - after: true, - }, - ], - 'comma-style': ['warn', 'last'], 'consistent-return': 'error', 'dot-notation': 'error', - eqeqeq: 'error', - indent: ['warn', 2], + eqeqeq: ['error', 'always', { 'null': 'ignore' }], 'jsx-quotes': ['error', 'prefer-single'], 'no-case-declarations': 'off', 'no-catch-shadow': 'error', @@ -80,88 +76,74 @@ module.exports = { }, ], 'no-empty': 'off', - 'no-nested-ternary': 'warn', - 'no-prototype-builtins': 'off', 'no-restricted-properties': [ 'error', { property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substr', message: 'Use .slice instead of .substr.' }, ], + 'no-restricted-syntax': [ + 'error', + { + // eslint-disable-next-line no-restricted-syntax + selector: 'Literal[value=/•/], JSXText[value=/•/]', + // eslint-disable-next-line no-restricted-syntax + message: "Use '·' (middle dot) instead of '•' (bullet)", + }, + ], 'no-self-assign': 'off', - 'no-trailing-spaces': 'warn', 'no-unused-expressions': 'error', - 'no-unused-vars': [ + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ 'error', { vars: 'all', args: 'after-used', + destructuredArrayIgnorePattern: '^_', ignoreRestSiblings: true, }, ], - 'no-useless-escape': 'off', - 'object-curly-spacing': ['error', 'always'], - 'padded-blocks': [ - 'error', - { - classes: 'always', - }, - ], - quotes: ['error', 'single'], - semi: 'error', 'valid-typeof': 'error', + 'react/jsx-filename-extension': ['error', { extensions: ['.jsx', 'tsx'] }], 'react/jsx-boolean-value': 'error', - 'react/jsx-closing-bracket-location': ['error', 'line-aligned'], - 'react/jsx-curly-spacing': 'error', + 'react/display-name': 'off', + 'react/jsx-fragments': ['error', 'syntax'], 'react/jsx-equals-spacing': 'error', - 'react/jsx-first-prop-new-line': ['error', 'multiline-multiprop'], - 'react/jsx-indent': ['error', 2], 'react/jsx-no-bind': 'error', - 'react/jsx-no-duplicate-props': 'error', - 'react/jsx-no-undef': 'error', + 'react/jsx-no-useless-fragment': 'error', + 'react/jsx-no-target-blank': 'off', 'react/jsx-tag-spacing': 'error', - 'react/jsx-uses-react': 'error', - 'react/jsx-uses-vars': 'error', + 'react/jsx-uses-react': 'off', // not needed with new JSX transform 'react/jsx-wrap-multilines': 'error', - 'react/no-multi-comp': 'off', - 'react/no-string-refs': 'error', - 'react/prop-types': 'error', + 'react/no-deprecated': 'off', + 'react/no-unknown-property': 'off', + 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/self-closing-comp': 'error', + // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js 'jsx-a11y/accessible-emoji': 'warn', - 'jsx-a11y/alt-text': 'warn', - 'jsx-a11y/anchor-has-content': 'warn', - 'jsx-a11y/anchor-is-valid': [ - 'warn', - { - components: [ - 'Link', - 'NavLink', - ], - specialLink: [ - 'to', - ], - aspect: [ - 'noHref', - 'invalidHref', - 'preferButton', - ], - }, - ], - 'jsx-a11y/aria-activedescendant-has-tabindex': 'warn', - 'jsx-a11y/aria-props': 'warn', - 'jsx-a11y/aria-proptypes': 'warn', - 'jsx-a11y/aria-role': 'warn', - 'jsx-a11y/aria-unsupported-elements': 'warn', - 'jsx-a11y/heading-has-content': 'warn', - 'jsx-a11y/html-has-lang': 'warn', - 'jsx-a11y/iframe-has-title': 'warn', - 'jsx-a11y/img-redundant-alt': 'warn', - 'jsx-a11y/interactive-supports-focus': 'warn', - 'jsx-a11y/label-has-for': 'off', - 'jsx-a11y/mouse-events-have-key-events': 'warn', - 'jsx-a11y/no-access-key': 'warn', - 'jsx-a11y/no-distracting-elements': 'warn', + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/label-has-associated-control': 'off', + 'jsx-a11y/media-has-caption': 'off', + 'jsx-a11y/no-autofocus': 'off', + // recommended rule is: + // 'jsx-a11y/no-interactive-element-to-noninteractive-role': [ + // 'error', + // { + // tr: ['none', 'presentation'], + // canvas: ['img'], + // }, + // ], + 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off', + // recommended rule is: + // 'jsx-a11y/no-noninteractive-element-interactions': [ + // 'error', + // { + // body: ['onError', 'onLoad'], + // iframe: ['onError', 'onLoad'], + // img: ['onError', 'onLoad'], + // }, + // ], 'jsx-a11y/no-noninteractive-element-interactions': [ 'warn', { @@ -170,8 +152,18 @@ module.exports = { ], }, ], + // recommended rule is: + // 'jsx-a11y/no-noninteractive-tabindex': [ + // 'error', + // { + // tags: [], + // roles: ['tabpanel'], + // allowExpressionValues: true, + // }, + // ], + 'jsx-a11y/no-noninteractive-tabindex': 'off', 'jsx-a11y/no-onchange': 'warn', - 'jsx-a11y/no-redundant-roles': 'warn', + // recommended is full 'error' 'jsx-a11y/no-static-element-interactions': [ 'warn', { @@ -180,37 +172,212 @@ module.exports = { ], }, ], - 'jsx-a11y/role-has-required-aria-props': 'warn', - 'jsx-a11y/role-supports-aria-props': 'off', - 'jsx-a11y/scope': 'warn', - 'jsx-a11y/tabindex-no-positive': 'warn', + // See https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js 'import/extensions': [ 'error', 'always', { js: 'never', + jsx: 'never', + mjs: 'never', + ts: 'never', + tsx: 'never', }, ], + 'import/first': 'error', 'import/newline-after-import': 'error', + 'import/no-anonymous-default-export': 'error', 'import/no-extraneous-dependencies': [ 'error', { devDependencies: [ 'config/webpack/**', + 'app/javascript/mastodon/performance.js', 'app/javascript/mastodon/test_setup.js', 'app/javascript/**/__tests__/**', ], }, ], - 'import/no-unresolved': 'error', + 'import/no-amd': 'error', + 'import/no-commonjs': 'error', + 'import/no-import-module-exports': 'error', + 'import/no-relative-packages': 'error', + 'import/no-self-import': 'error', + 'import/no-useless-path-segments': 'error', 'import/no-webpack-loader-syntax': 'error', + 'import/order': [ + 'error', + { + alphabetize: { order: 'asc' }, + 'newlines-between': 'always', + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + ['index', 'sibling'], + 'object', + ], + pathGroups: [ + // React core packages + { + pattern: '{react,react-dom,react-dom/client,prop-types}', + group: 'builtin', + position: 'after', + }, + // I18n + { + pattern: '{react-intl,intl-messageformat}', + group: 'builtin', + position: 'after', + }, + // Common React utilities + { + pattern: '{classnames,react-helmet,react-router-dom}', + group: 'external', + position: 'before', + }, + // Immutable / Redux / data store + { + pattern: '{immutable,react-redux,react-immutable-proptypes,react-immutable-pure-component,reselect}', + group: 'external', + position: 'before', + }, + // Internal packages + { + pattern: '{mastodon/**,flavours/glitch-soc/**}', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: [], + }, + ], + + 'promise/always-return': 'off', 'promise/catch-or-return': [ 'error', { allowFinally: true, }, ], + 'promise/no-callback-in-promise': 'off', + 'promise/no-nesting': 'off', + 'promise/no-promise-in-callback': 'off', + + 'formatjs/blocklist-elements': 'error', + 'formatjs/enforce-default-message': ['error', 'literal'], + 'formatjs/enforce-description': 'off', // description values not currently used + 'formatjs/enforce-id': 'off', // Explicit IDs are used in the project + 'formatjs/enforce-placeholders': 'off', // Issues in short_number.jsx + 'formatjs/enforce-plural-rules': 'error', + 'formatjs/no-camel-case': 'off', // disabledAccount is only non-conforming + 'formatjs/no-complex-selectors': 'error', + 'formatjs/no-emoji': 'error', + 'formatjs/no-id': 'off', // IDs are used for translation keys + 'formatjs/no-invalid-icu': 'error', + 'formatjs/no-literal-string-in-jsx': 'off', // Should be looked at, but mainly flagging punctuation outside of strings + 'formatjs/no-multiple-plurals': 'off', // Only used by hashtag.jsx + 'formatjs/no-multiple-whitespaces': 'error', + 'formatjs/no-offset': 'error', + 'formatjs/no-useless-message': 'error', + 'formatjs/prefer-formatted-message': 'error', + 'formatjs/prefer-pound-in-plural': 'error', + + 'jsdoc/check-types': 'off', + 'jsdoc/no-undefined-types': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-returns': 'off', }, + + overrides: [ + { + files: [ + '*.config.js', + '.*rc.js', + 'ide-helper.js', + 'config/webpack/**/*', + 'config/formatjs-formatter.js', + ], + + env: { + commonjs: true, + }, + + parserOptions: { + sourceType: 'script', + }, + + rules: { + 'import/no-commonjs': 'off', + }, + }, + { + files: [ + '**/*.ts', + '**/*.tsx', + ], + + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:promise/recommended', + 'plugin:jsdoc/recommended-typescript', + 'plugin:prettier/recommended', + ], + + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + }, + + rules: { + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + + '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], + '@typescript-eslint/consistent-type-exports': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + + 'jsdoc/require-jsdoc': 'off', + + // Those rules set stricter rules for TS files + // to enforce better practices when converting from JS + 'import/no-default-export': 'warn', + 'react/prefer-stateless-function': 'warn', + 'react/function-component-definition': ['error', { namedComponents: 'arrow-function' }], + 'react/jsx-uses-react': 'off', // not needed with new JSX transform + 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform + 'react/prop-types': 'off', + }, + }, + { + files: [ + '**/__tests__/*.js', + '**/__tests__/*.jsx', + ], + + env: { + jest: true, + }, + }, + { + files: [ + 'streaming/**/*', + ], + rules: { + 'import/no-commonjs': 'off', + }, + }, + ], }; diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 0000000000..1ae40d4161 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,114 @@ +{ + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: [ + 'config:base', + ':dependencyDashboard', + ':labels(dependencies)', + ':maintainLockFilesMonthly', // update non-direct dependencies monthly + ':prConcurrentLimit10', // only 10 open PRs at the same time + ], + stabilityDays: 3, // Wait 3 days after the package has been published before upgrading it + // packageRules order is important, they are applied from top to bottom and are merged, + // so for example grouping rules needs to be at the bottom + packageRules: [ + { + // Ignore major version bumps for these node packages + matchManagers: ['npm'], + matchPackageNames: [ + '@rails/ujs', // Needs to match the major Rails version + 'tesseract.js', // Requires code changes + 'react-hotkeys', // Requires code changes + + // Requires Webpacker upgrade or replacement + '@types/webpack', + 'babel-loader', + 'compression-webpack-plugin', + 'css-loader', + 'imports-loader', + 'mini-css-extract-plugin', + 'postcss-loader', + 'sass-loader', + 'terser-webpack-plugin', + 'webpack', + 'webpack-assets-manifest', + 'webpack-bundle-analyzer', + 'webpack-dev-server', + 'webpack-cli', + + // react-router: Requires manual upgrade + 'history', + 'react-router-dom', + ], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Ignore major version bumps for these Ruby packages + matchManagers: ['bundler'], + matchPackageNames: [ + 'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x + 'strong_migrations', // Requires manual upgrade + 'sidekiq', // Requires manual upgrade + 'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version + 'redis', // Requires manual upgrade and sync with Sidekiq version + 'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964 + + // Needs major Rails version bump + 'rack', + 'rails', + 'rails-i18n', + ], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Update Github Actions and Docker images weekly + matchManagers: ['github-actions', 'dockerfile', 'docker-compose'], + extends: ['schedule:weekly'], + }, + { + // Ignore major & minor bumps for the ruby image, this needs to be synced with .ruby-version + matchManagers: ['dockerfile'], + matchPackageNames: ['moritzheiber/ruby-jemalloc'], + matchUpdateTypes: ['minor', 'major'], + enabled: false, + }, + { + // Ignore major bump for the node image, this needs to be synced with .nvmrc + matchManagers: ['dockerfile'], + matchPackageNames: ['node'], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Ignore major postgres bumps in the docker-compose file, as those break dev environments + matchManagers: ['docker-compose'], + matchPackageNames: ['postgres'], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Update devDependencies every week, with one grouped PR + matchDepTypes: 'devDependencies', + matchUpdateTypes: ['patch', 'minor'], + excludePackageNames: [ + 'typescript', // Typescript has many changes in minor versions, needs to be checked every time + ], + groupName: 'devDependencies (non-major)', + extends: ['schedule:weekly'], + }, + { + // Update @types/* packages every week, with one grouped PR + matchPackagePrefixes: '@types/', + matchUpdateTypes: ['patch', 'minor'], + groupName: 'DefinitelyTyped types (non-major)', + extends: ['schedule:weekly'], + addLabels: ['typescript'], + }, + // Add labels depending on package manager + { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] }, + { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] }, + { matchManagers: ['docker-compose', 'dockerfile'], addLabels: ['docker'] }, + { matchManagers: ['github-actions'], addLabels: ['github_actions'] }, + ], +} diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index bf50afe8c7..da4203e357 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -15,17 +15,25 @@ permissions: jobs: build-image: runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: - uses: actions/checkout@v3 - - uses: hadolint/hadolint-action@v3.0.0 + - uses: hadolint/hadolint-action@v3.1.0 - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - - uses: docker/login-action@v2 + + - name: Log in to the Github Container registry + uses: docker/login-action@v2 with: registry: ghcr.io - username: ${{ github.repository_owner }} + username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} if: github.event_name != 'pull_request' + - uses: docker/metadata-action@v4 id: meta with: @@ -34,12 +42,22 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=edge,branch=main type=sha,prefix=,format=long - - uses: docker/build-push-action@v3 + + - name: Generate version suffix + id: version_vars + if: github.repository == 'mastodon/mastodon' && github.event_name == 'push' && github.ref_name == 'main' + run: | + echo mastodon_version_suffix=+edge-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT + + - uses: docker/build-push-action@v4 with: context: . + build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} platforms: linux/amd64,linux/arm64 + provenance: false builder: ${{ steps.buildx.outputs.name }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml new file mode 100644 index 0000000000..f07f7447ca --- /dev/null +++ b/.github/workflows/build-nightly.yml @@ -0,0 +1,60 @@ +name: Build nightly container image +on: + workflow_dispatch: + schedule: + - cron: '0 2 * * *' # run at 2 AM UTC +permissions: + contents: read + packages: write + +jobs: + build-nightly-image: + runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v3 + - uses: hadolint/hadolint-action@v3.1.0 + - uses: docker/setup-qemu-action@v2 + - uses: docker/setup-buildx-action@v2 + + - name: Log in to the Github Container registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v4 + id: meta + with: + images: | + ghcr.io/mastodon/mastodon + flavor: | + latest=auto + tags: | + type=raw,value=nightly + type=schedule,pattern=nightly-{{date 'YYYY-MM-DD' tz='Etc/UTC'}} + labels: | + org.opencontainers.image.description=Nightly build image used for testing purposes + + - name: Generate version suffix + id: version_vars + run: | + echo mastodon_version_suffix=+nightly-$(date +'%Y%m%d') >> $GITHUB_OUTPUT + + - uses: docker/build-push-action@v4 + with: + context: . + build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} + platforms: linux/amd64,linux/arm64 + provenance: false + builder: ${{ steps.buildx.outputs.name }} + push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index 9a74630607..b67c503e95 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -14,24 +14,49 @@ permissions: jobs: check-i18n: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 + - name: Install system dependencies run: | sudo apt-get update 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: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Check for missing strings in English JSON + run: | + yarn i18n:extract --throws + git diff --exit-code + - name: Check locale file normalization run: bundle exec i18n-tasks check-normalized + - name: Check for unused strings - run: bundle exec i18n-tasks unused -l en + run: bundle exec i18n-tasks unused + + - name: Check for missing strings in English YML + run: | + bundle exec i18n-tasks add-missing -l en + git diff --exit-code + - name: Check for wrong string interpolations run: bundle exec i18n-tasks check-consistent-interpolations + - name: Check that all required locale files exist run: bundle exec rake repo:check_locales_files diff --git a/.github/workflows/haml-lint-problem-matcher.json b/.github/workflows/haml-lint-problem-matcher.json new file mode 100644 index 0000000000..3523ea2951 --- /dev/null +++ b/.github/workflows/haml-lint-problem-matcher.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "haml-lint", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+)\\s\\[W]\\s(.*):\\s(.*)$", + "file": 1, + "line": 2, + "code": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml new file mode 100644 index 0000000000..4d3c2ce5af --- /dev/null +++ b/.github/workflows/lint-css.yml @@ -0,0 +1,52 @@ +name: CSS Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '.prettier*' + - 'stylelint.config.js' + - '**/*.css' + - '**/*.scss' + - '.github/workflows/lint-css.yml' + - '.github/stylelint-matcher.json' + + pull_request: + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '.prettier*' + - 'stylelint.config.js' + - '**/*.css' + - '**/*.scss' + - '.github/workflows/lint-css.yml' + - '.github/stylelint-matcher.json' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - uses: xt0rted/stylelint-problem-matcher@v1 + + - run: echo "::add-matcher::.github/stylelint-matcher.json" + + - name: Stylelint + run: yarn lint:sass diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml new file mode 100644 index 0000000000..56d817123a --- /dev/null +++ b/.github/workflows/lint-haml.yml @@ -0,0 +1,47 @@ +name: Haml Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - '.github/workflows/haml-lint-problem-matcher.json' + - '.github/workflows/lint-haml.yml' + - '.haml-lint*.yml' + - '.rubocop*.yml' + - '.ruby-version' + - '**/*.haml' + - 'Gemfile*' + + pull_request: + paths: + - '.github/workflows/haml-lint-problem-matcher.json' + - '.github/workflows/lint-haml.yml' + - '.haml-lint*.yml' + - '.rubocop*.yml' + - '.ruby-version' + - '**/*.haml' + - 'Gemfile*' + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + 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 haml-lint + run: | + echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json" + bundle exec haml-lint diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml new file mode 100644 index 0000000000..1f0cfd1e70 --- /dev/null +++ b/.github/workflows/lint-js.yml @@ -0,0 +1,55 @@ +name: JavaScript Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - '.nvmrc' + - '.prettier*' + - '.eslint*' + - '**/*.js' + - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' + - '.github/workflows/lint-js.yml' + + pull_request: + paths: + - 'package.json' + - 'yarn.lock' + - 'tsconfig.json' + - '.nvmrc' + - '.prettier*' + - '.eslint*' + - '**/*.js' + - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' + - '.github/workflows/lint-js.yml' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: ESLint + run: yarn lint:js --max-warnings 0 + + - name: Typecheck + run: yarn typecheck diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml index 5bf4349b3d..8712d8bd80 100644 --- a/.github/workflows/lint-json.yml +++ b/.github/workflows/lint-json.yml @@ -3,20 +3,25 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - '**/*.json' - '.github/workflows/lint-json.yml' + - '!app/javascript/mastodon/locales/*.json' pull_request: paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - '**/*.json' - '.github/workflows/lint-json.yml' + - '!app/javascript/mastodon/locales/*.json' jobs: lint: @@ -30,9 +35,10 @@ jobs: uses: actions/setup-node@v3 with: cache: yarn + node-version-file: '.nvmrc' - name: Install all yarn packages run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.json" + run: yarn lint:json diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml new file mode 100644 index 0000000000..d19a0470db --- /dev/null +++ b/.github/workflows/lint-md.yml @@ -0,0 +1,44 @@ +name: Markdown Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - '.github/workflows/lint-md.yml' + - '.nvmrc' + - '.prettier*' + - '**/*.md' + - '!AUTHORS.md' + - 'package.json' + - 'yarn.lock' + + pull_request: + paths: + - '.github/workflows/lint-md.yml' + - '.nvmrc' + - '.prettier*' + - '**/*.md' + - '!AUTHORS.md' + - 'package.json' + - 'yarn.lock' + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Prettier + run: yarn lint:md diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml new file mode 100644 index 0000000000..0395c8639f --- /dev/null +++ b/.github/workflows/lint-ruby.yml @@ -0,0 +1,50 @@ +name: Ruby Linting +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - 'Gemfile*' + - '.rubocop*.yml' + - '.ruby-version' + - '.bundler-audit.yml' + - '**/*.rb' + - '**/*.rake' + - '.github/workflows/lint-ruby.yml' + + pull_request: + paths: + - 'Gemfile*' + - '.rubocop*.yml' + - '.ruby-version' + - '.bundler-audit.yml' + - '**/*.rb' + - '**/*.rake' + - '.github/workflows/lint-ruby.yml' + +jobs: + lint: + 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: Set-up RuboCop Problem Matcher + uses: r7kamura/rubocop-problem-matchers-action@v1 + + - name: Run rubocop + run: bundle exec rubocop + + - name: Run bundler-audit + run: bundle exec bundler-audit diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml index b939ec8ce2..295e9610b3 100644 --- a/.github/workflows/lint-yml.yml +++ b/.github/workflows/lint-yml.yml @@ -3,22 +3,27 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - '**/*.yaml' - '**/*.yml' - '.github/workflows/lint-yml.yml' + - '!config/locales/*.yml' pull_request: paths: - 'package.json' - 'yarn.lock' + - '.nvmrc' - '.prettier*' - '**/*.yaml' - '**/*.yml' - '.github/workflows/lint-yml.yml' + - '!config/locales/*.yml' jobs: lint: @@ -32,9 +37,10 @@ jobs: uses: actions/setup-node@v3 with: cache: yarn + node-version-file: '.nvmrc' - name: Install all yarn packages run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.{yml,yaml}" + run: yarn lint:yml diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml deleted file mode 100644 index b6438d6659..0000000000 --- a/.github/workflows/linter.yml +++ /dev/null @@ -1,83 +0,0 @@ ---- -################################# -################################# -## Super Linter GitHub Actions ## -################################# -################################# -name: Lint Code Base - -# -# Documentation: -# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions -# - -############################# -# Start the job on all push # -############################# -on: - push: - branches-ignore: [main] - # Remove the line above to run when pushing to master - pull_request: - branches: [main] - -############### -# Set the Job # -############### -permissions: - checks: write - contents: read - pull-requests: write - statuses: write - -jobs: - build: - # Name the Job - name: Lint Code Base - # Set the agent to run on - runs-on: ubuntu-latest - - ################## - # Load all steps # - ################## - steps: - ########################## - # Checkout the code base # - ########################## - - name: Checkout Code - uses: actions/checkout@v3 - with: - # Full git history is needed to get a proper list of changed files within `super-linter` - fetch-depth: 0 - - - name: Set-up Node.js - uses: actions/setup-node@v3 - with: - node-version-file: .nvmrc - cache: yarn - - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Set-up RuboCop Problem Mathcher - uses: r7kamura/rubocop-problem-matchers-action@v1 - - name: Set-up Stylelint Problem Matcher - uses: xt0rted/stylelint-problem-matcher@v1 - # https://github.com/xt0rted/stylelint-problem-matcher/issues/360 - - run: echo "::add-matcher::.github/stylelint-matcher.json" - - ################################ - # Run Linter against code base # - ################################ - - name: Lint Code Base - uses: github/super-linter@v4 - env: - CSS_FILE_NAME: stylelint.config.js - DEFAULT_BRANCH: main - NO_COLOR: 1 # https://github.com/xt0rted/stylelint-problem-matcher/issues/360 - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - JAVASCRIPT_ES_CONFIG_FILE: .eslintrc.js - LINTER_RULES_PATH: . - RUBY_CONFIG_FILE: .rubocop.yml - VALIDATE_ALL_CODEBASE: false - VALIDATE_CSS: true - VALIDATE_JAVASCRIPT_ES: true - VALIDATE_RUBY: true diff --git a/.github/workflows/rebase-needed.yml b/.github/workflows/rebase-needed.yml index 6f903ee610..131a62a576 100644 --- a/.github/workflows/rebase-needed.yml +++ b/.github/workflows/rebase-needed.yml @@ -2,16 +2,35 @@ name: PR Needs Rebase on: push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + - 'l10n_main' pull_request_target: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + - 'l10n_main' types: [synchronize] +permissions: + pull-requests: write + jobs: label-rebase-needed: runs-on: ubuntu-latest + + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: - name: Check for merge conflicts uses: eps1lon/actions-label-merge-conflict@releases/2.x with: dirtyLabel: 'rebase needed :construction:' repoToken: '${{ secrets.GITHUB_TOKEN }}' + commentOnClean: This pull request has resolved merge conflicts and is ready for review. commentOnDirty: This pull request has merge conflicts that must be resolved before it can be merged. + retryMax: 10 + continueOnMissingPermissions: false diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml new file mode 100644 index 0000000000..3306105f9e --- /dev/null +++ b/.github/workflows/test-js.yml @@ -0,0 +1,48 @@ +name: JavaScript Testing +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '**/*.js' + - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' + - '**/*.snap' + - '.github/workflows/test-js.yml' + + pull_request: + paths: + - 'package.json' + - 'yarn.lock' + - '.nvmrc' + - '**/*.js' + - '**/*.jsx' + - '**/*.ts' + - '**/*.tsx' + - '**/*.snap' + - '.github/workflows/test-js.yml' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Clone repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install all yarn packages + run: yarn --frozen-lockfile + + - name: Jest testing + run: yarn jest --reporters github-actions summary diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml new file mode 100644 index 0000000000..a91fd819a2 --- /dev/null +++ b/.github/workflows/test-migrations-one-step.yml @@ -0,0 +1,111 @@ +name: Test one step migrations +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + pull_request: + +jobs: + pre_job: + runs-on: ubuntu-latest + + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-one-step.yml", "lib/tasks/tests.rake"]' + + test: + runs-on: ubuntu-latest + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' + + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + + services: + postgres: + image: postgres:${{ matrix.postgres}} + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + CONTINUOUS_INTEGRATION: true + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + BUNDLE_WITHOUT: 'development production' + BUNDLE_JOBS: 3 + BUNDLE_RETRY: 3 + + steps: + - uses: actions/checkout@v3 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Create database + run: './bin/rails db:create' + + - name: Run migrations up to v2.0.0 + run: './bin/rails db:migrate VERSION=20171010025614' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2' + + - name: Run migrations up to v2.4.0 + run: './bin/rails db:migrate VERSION=20180514140000' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4' + + - name: Run migrations up to v2.4.3 + run: './bin/rails db:migrate VERSION=20180707154237' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4_3' + + - name: Run all remaining migrations + run: './bin/rails db:migrate' + + - name: Check migration result + run: './bin/rails tests:migrations:check_database' diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml new file mode 100644 index 0000000000..50266fb8a0 --- /dev/null +++ b/.github/workflows/test-migrations-two-step.yml @@ -0,0 +1,119 @@ +name: Test two step migrations +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + pull_request: + +jobs: + pre_job: + runs-on: ubuntu-latest + + outputs: + should_skip: ${{ steps.skip_check.outputs.should_skip }} + + steps: + - id: skip_check + uses: fkirc/skip-duplicate-actions@v5 + with: + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-two-step.yml", "lib/tasks/tests.rake"]' + + test: + runs-on: ubuntu-latest + needs: pre_job + if: needs.pre_job.outputs.should_skip != 'true' + + strategy: + fail-fast: false + + matrix: + postgres: + - 14-alpine + - 15-alpine + + services: + postgres: + image: postgres:${{ matrix.postgres}} + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + CONTINUOUS_INTEGRATION: true + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + BUNDLE_WITHOUT: 'development production' + BUNDLE_JOBS: 3 + BUNDLE_RETRY: 3 + + steps: + - uses: actions/checkout@v3 + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Create database + run: './bin/rails db:create' + + - name: Run migrations up to v2.0.0 + run: './bin/rails db:migrate VERSION=20171010025614' + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2' + + - name: Run pre-deployment migrations up to v2.4.0 + run: './bin/rails db:migrate VERSION=20180514140000' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4' + + - name: Run migrations up to v2.4.3 + run: './bin/rails db:migrate VERSION=20180707154237' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Populate database with test data + run: './bin/rails tests:migrations:populate_v2_4_3' + + - name: Run all remaining pre-deployment migrations + run: './bin/rails db:migrate' + env: + SKIP_POST_DEPLOYMENT_MIGRATIONS: true + + - name: Run all post-deployment migrations + run: './bin/rails db:migrate' + + - name: Check migration result + run: './bin/rails tests:migrations:check_database' diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml new file mode 100644 index 0000000000..07cb1d41f8 --- /dev/null +++ b/.github/workflows/test-ruby.yml @@ -0,0 +1,151 @@ +name: Ruby Testing + +on: + push: + branches-ignore: + - 'dependabot/**' + - 'renovate/**' + pull_request: + +env: + BUNDLE_CLEAN: true + BUNDLE_FROZEN: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + mode: + - production + - test + env: + RAILS_ENV: ${{ matrix.mode }} + BUNDLE_WITH: ${{ matrix.mode }} + OTP_SECRET: precompile_placeholder + SECRET_KEY_BASE: precompile_placeholder + + steps: + - uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install native Ruby dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - run: yarn --frozen-lockfile --production + - name: Precompile assets + # Previously had set this, but it's not supported + # export NODE_OPTIONS=--openssl-legacy-provider + run: |- + ./bin/rails assets:precompile + + - uses: actions/upload-artifact@v3 + if: matrix.mode == 'test' + with: + path: |- + ./public/assets + ./public/packs-test + name: ${{ github.sha }} + retention-days: 0 + + test: + runs-on: ubuntu-latest + + needs: + - build + + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + env: + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + ALLOW_NOPAM: true + PAM_ENABLED: true + PAM_DEFAULT_SERVICE: pam_test + PAM_CONTROLLED_SERVICE: pam_test_controlled + BUNDLE_WITH: 'pam_authentication test' + CI_JOBS: ${{ matrix.ci_job }}/4 + + strategy: + fail-fast: false + matrix: + ruby-version: + - '3.0' + - '3.1' + - '.ruby-version' + ci_job: + - 1 + - 2 + - 3 + - 4 + steps: + - uses: actions/checkout@v3 + + - uses: actions/download-artifact@v3 + with: + path: './public' + name: ${{ github.sha }} + + - name: Update package index + run: sudo apt-get update + + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Install additional system dependencies + run: sudo apt-get install -y ffmpeg imagemagick libpam-dev + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version}} + bundler-cache: true + + - name: Load database schema + run: './bin/rails db:create db:schema:load db:seed' + + - run: bundle exec rake rspec_chunked diff --git a/.haml-lint.yml b/.haml-lint.yml index 7853d81d7c..d1ed30b260 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -1,108 +1,14 @@ -# Whether to ignore frontmatter at the beginning of HAML documents for -# frameworks such as Jekyll/Middleman -skip_frontmatter: false +inherits_from: .haml-lint_todo.yml exclude: - 'vendor/**/*' - - 'spec/**/*' - - 'lib/templates/**/*' - - 'app/views/kaminari/**/*' + - lib/templates/haml/scaffold/_form.html.haml + +require: + - ./lib/linter/haml_middle_dot.rb linters: AltText: - enabled: false - - ClassAttributeWithStaticValue: enabled: true - - ClassesBeforeIds: - enabled: true - - ConsecutiveComments: - enabled: true - - ConsecutiveSilentScripts: - enabled: true - max_consecutive: 2 - - EmptyObjectReference: - enabled: true - - EmptyScript: - enabled: true - - FinalNewline: - enabled: true - present: true - - HtmlAttributes: - enabled: true - - ImplicitDiv: - enabled: true - - LeadingCommentSpace: - enabled: true - - LineLength: - enabled: false - max: 80 - - MultilinePipe: - enabled: true - - MultilineScript: - enabled: true - - ObjectReferenceAttributes: - enabled: true - - RuboCop: - enabled: true - # These cops are incredibly noisy when it comes to HAML templates, so we - # ignore them. - ignored_cops: - - Lint/BlockAlignment - - Lint/EndAlignment - - Lint/Void - - Metrics/BlockLength - - Metrics/LineLength - - Style/AlignParameters - - Style/BlockNesting - - Style/ElseAlignment - - Style/EndOfLine - - Style/FileName - - Style/FinalNewline - - Style/FrozenStringLiteralComment - - Style/IfUnlessModifier - - Style/IndentationWidth - - Style/Next - - Style/TrailingBlankLines - - Style/TrailingWhitespace - - Style/WhileUntilModifier - - RubyComments: - enabled: true - - SpaceBeforeScript: - enabled: true - - SpaceInsideHashAttributes: - enabled: true - style: space - - Indentation: - enabled: true - character: space # or tab - - TagName: - enabled: true - - TrailingWhitespace: - enabled: true - - UnnecessaryInterpolation: - enabled: true - - UnnecessaryStringOutput: + MiddleDot: enabled: true diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml new file mode 100644 index 0000000000..c601683907 --- /dev/null +++ b/.haml-lint_todo.yml @@ -0,0 +1,106 @@ +# This configuration was generated by +# `haml-lint --auto-gen-config` +# on 2023-03-15 00:55:01 -0400 using Haml-Lint version 0.45.0. +# The point is for the user to remove these configuration records +# one by one as the lints are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of Haml-Lint, may require this file to be generated again. + +linters: + # Offense count: 63 + RuboCop: + exclude: + - 'app/views/accounts/_og.html.haml' + - 'app/views/admin/account_warnings/_account_warning.html.haml' + - 'app/views/admin/accounts/index.html.haml' + - 'app/views/admin/accounts/show.html.haml' + - 'app/views/admin/announcements/edit.html.haml' + - 'app/views/admin/announcements/new.html.haml' + - 'app/views/admin/disputes/appeals/_appeal.html.haml' + - 'app/views/admin/domain_blocks/edit.html.haml' + - 'app/views/admin/domain_blocks/new.html.haml' + - 'app/views/admin/ip_blocks/new.html.haml' + - 'app/views/admin/reports/actions/preview.html.haml' + - 'app/views/admin/reports/index.html.haml' + - 'app/views/admin/reports/show.html.haml' + - 'app/views/admin/roles/_form.html.haml' + - 'app/views/admin/settings/about/show.html.haml' + - 'app/views/admin/settings/appearance/show.html.haml' + - 'app/views/admin/settings/registrations/show.html.haml' + - 'app/views/admin/statuses/show.html.haml' + - 'app/views/auth/registrations/new.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + - 'app/views/filters/_filter_fields.html.haml' + - 'app/views/invites/_form.html.haml' + - 'app/views/layouts/application.html.haml' + - 'app/views/layouts/error.html.haml' + - 'app/views/notification_mailer/_status.html.haml' + - 'app/views/settings/applications/_fields.html.haml' + - 'app/views/settings/imports/show.html.haml' + - 'app/views/settings/preferences/appearance/show.html.haml' + - 'app/views/settings/preferences/other/show.html.haml' + - 'app/views/statuses/_detailed_status.html.haml' + - 'app/views/statuses/_poll.html.haml' + - 'app/views/statuses/show.html.haml' + - 'app/views/statuses_cleanup/show.html.haml' + - 'app/views/user_mailer/warning.html.haml' + + # Offense count: 913 + LineLength: + enabled: false + + # Offense count: 22 + UnnecessaryStringOutput: + exclude: + - 'app/views/accounts/show.html.haml' + - 'app/views/admin/custom_emojis/_custom_emoji.html.haml' + - 'app/views/admin/relays/_relay.html.haml' + - 'app/views/admin/rules/_rule.html.haml' + - 'app/views/admin/statuses/index.html.haml' + - 'app/views/auth/registrations/_sessions.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + - 'app/views/notification_mailer/_status.html.haml' + - 'app/views/settings/two_factor_authentication_methods/index.html.haml' + - 'app/views/statuses/_detailed_status.html.haml' + - 'app/views/statuses/_poll.html.haml' + - 'app/views/statuses/_simple_status.html.haml' + - 'app/views/user_mailer/suspicious_sign_in.html.haml' + - 'app/views/user_mailer/webauthn_credential_added.html.haml' + - 'app/views/user_mailer/webauthn_credential_deleted.html.haml' + - 'app/views/user_mailer/welcome.html.haml' + + # Offense count: 3 + ViewLength: + exclude: + - 'app/views/admin/accounts/show.html.haml' + - 'app/views/admin/reports/show.html.haml' + - 'app/views/disputes/strikes/show.html.haml' + + # Offense count: 41 + InstanceVariables: + exclude: + - 'app/views/admin/reports/_actions.html.haml' + - 'app/views/admin/roles/_form.html.haml' + - 'app/views/admin/webhooks/_form.html.haml' + - 'app/views/auth/registrations/_sessions.html.haml' + - 'app/views/auth/registrations/_status.html.haml' + - 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml' + - 'app/views/authorize_interactions/_post_follow_actions.html.haml' + - 'app/views/invites/_form.html.haml' + - 'app/views/relationships/_account.html.haml' + - 'app/views/shared/_og.html.haml' + - 'app/views/statuses/_status.html.haml' + + # Offense count: 6 + ConsecutiveSilentScripts: + exclude: + - 'app/views/admin/settings/shared/_links.html.haml' + - 'app/views/settings/login_activities/_login_activity.html.haml' + - 'app/views/statuses/_poll.html.haml' + + # Offense count: 3 + IdNames: + exclude: + - 'app/views/authorize_interactions/error.html.haml' + - 'app/views/oauth/authorizations/error.html.haml' + - 'app/views/shared/_error_messages.html.haml' diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..d2ae35e84b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.nvmrc b/.nvmrc index b6a7d89c68..59ea99ee63 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 +16.20 diff --git a/.prettierignore b/.prettierignore index dc8c536933..27b6d5458a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -51,15 +51,8 @@ *~ *.swp -# Ignore npm debug log -npm-debug.log - -# Ignore yarn log files -yarn-error.log -yarn-debug.log - -# Ignore vagrant log files -*-cloudimg-console.log +# Ignore log files +*.log # Ignore Docker option files docker-compose.override.yml @@ -68,12 +61,31 @@ docker-compose.override.yml /app/javascript/mastodon/features/emoji/emoji_map.json # Ignore locale files -/app/javascript/mastodon/locales +/app/javascript/mastodon/locales/*.json /config/locales +# Ignore vendored CSS reset +app/javascript/styles/mastodon/reset.scss + +# Ignore Javascript pending https://github.com/mastodon/mastodon/pull/23631 +*.js +*.jsx + +# Ignore HTML till cleaned and included in CI +*.html + +# Ignore the generated AUTHORS.md +AUTHORS.md + +# Ignore glitch-soc emoji map file +/app/javascript/flavours/glitch/features/emoji/emoji_map.json + # Ignore glitch-soc locale files /app/javascript/flavours/glitch/locales /config/locales-glitch -# Ignore glitch-soc emoji map file -/app/javascript/flavours/glitch/features/emoji/emoji_map.json +# Ignore glitch-soc vendored CSS reset +app/javascript/flavours/glitch/styles/reset.scss + +# Ignore win95 theme +app/javascript/styles/win95.scss diff --git a/.prettierrc.js b/.prettierrc.js index 1d70813d51..af39b253f6 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,3 +1,4 @@ module.exports = { - singleQuote: true + singleQuote: true, + jsxSingleQuote: true } diff --git a/.profile b/.profile index c6d57b609d..f4826ea303 100644 --- a/.profile +++ b/.profile @@ -1 +1 @@ -LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/app/.apt/lib/x86_64-linux-gnu:/app/.apt/usr/lib/x86_64-linux-gnu/mesa:/app/.apt/usr/lib/x86_64-linux-gnu/pulseaudio:/app/.apt/usr/lib/x86_64-linux-gnu/openblas-pthread diff --git a/.rubocop.yml b/.rubocop.yml index 3c92234705..eff89bdaee 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,479 +1,211 @@ +# Can be removed once all rules are addressed or moved to this file as documented overrides +inherit_from: .rubocop_todo.yml + +# Used for merging with exclude lists with .rubocop_todo.yml +inherit_mode: + merge: + - Exclude + require: - rubocop-rails - rubocop-rspec - rubocop-performance + - rubocop-capybara + - ./lib/linter/rubocop_middle_dot AllCops: - TargetRubyVersion: 2.7 + TargetRubyVersion: 3.0 # Set to minimum supported version of CI DisplayCopNames: true DisplayStyleGuide: true ExtraDetails: true UseCache: true CacheRootDirectory: tmp - NewCops: enable + NewCops: enable # Opt-in to newly added rules Exclude: - db/schema.rb - - 'app/views/**/*' - - 'config/**/*' - 'bin/*' - 'Rakefile' - 'node_modules/**/*' - 'Vagrantfile' - 'vendor/**/*' - - 'lib/json_ld/*' + - 'lib/json_ld/*' # Generated files - 'lib/templates/**/*' -Bundler/OrderedGems: - Enabled: false - -Layout/AccessModifierIndentation: - EnforcedStyle: indent - -Layout/EmptyLineAfterMagicComment: - Enabled: false - -Layout/EmptyLineAfterGuardClause: - Enabled: false - -Layout/EmptyLineBetweenDefs: - AllowAdjacentOneLineDefs: true - -Layout/EmptyLinesAroundAttributeAccessor: - Enabled: true - +# Reason: Prefer Hashes without extreme indentation +# https://docs.rubocop.org/rubocop/cops_layout.html#layoutfirsthashelementindentation Layout/FirstHashElementIndentation: EnforcedStyle: consistent -Layout/HashAlignment: - Enabled: false - -Layout/SpaceAroundMethodCallOperator: - Enabled: true - -Layout/SpaceInsideHashLiteralBraces: - EnforcedStyle: space - -Lint/DeprecatedOpenSSLConstant: - Enabled: true - -Lint/DuplicateElsifCondition: - Enabled: true - -Lint/MixedRegexpCaptureTypes: - Enabled: true - -Lint/RaiseException: - Enabled: true - -Lint/StructNewOverride: - Enabled: true - -Lint/UselessAccessModifier: - ContextCreatingMethods: - - class_methods - -Metrics/AbcSize: - Max: 34 # RuboCop default 17 - Exclude: - - 'lib/**/*cli*.rb' - - db/*migrate/**/* - - lib/paperclip/color_extractor.rb - - app/workers/scheduler/follow_recommendations_scheduler.rb - - app/services/activitypub/fetch*_service.rb - - lib/paperclip/**/* - CountRepeatedAttributes: false - AllowedMethods: - - update_media_attachments! - - account_link_to - - attempt_oembed - - build_crutches - - calculate_scores - - cc - - dump_actor! - - filter_from_home? - - hydrate - - import_bookmarks! - - import_relationships! - - initialize - - link_to_mention - - log_target - - matches_time_window? - - parse_metadata - - perform_statuses_search! - - privatize_media_attachments! - - process_update - - publish_media_attachments! - - remotable_attachment - - render_initial_state - - render_with_cache - - searchable_by - - self.cached_filters_for - - set_fetchable_attributes! - - signed_request_actor - - statuses_to_delete - - update_poll! - -Metrics/BlockLength: - Max: 55 - Exclude: - - 'lib/mastodon/*_cli.rb' - CountComments: false - CountAsOne: [array, heredoc] - AllowedMethods: - - task - - namespace - - class_methods - - included - -Metrics/BlockNesting: - Max: 3 - Exclude: - - 'lib/mastodon/*_cli.rb' - -Metrics/ClassLength: - CountComments: false - Max: 500 - CountAsOne: [array, heredoc] - Exclude: - - 'lib/mastodon/*_cli.rb' - -Metrics/CyclomaticComplexity: - Max: 12 - Exclude: - - lib/mastodon/*cli*.rb - - db/*migrate/**/* - AllowedMethods: - - attempt_oembed - - blocked? - - build_crutches - - calculate_scores - - cc - - discover_endpoint! - - filter_from_home? - - hydrate - - klass - - link_to_mention - - log_target - - matches_time_window? - - patch_for_forwarding! - - preprocess_attributes! - - process_update - - remotable_attachment - - scan_text! - - self.cached_filters_for - - set_fetchable_attributes! - - setup_redis_env_url - - update_media_attachments! - +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength Layout/LineLength: - Max: 140 # RuboCop default 120 - AllowHeredoc: true - AllowURI: true - IgnoreCopDirectives: true AllowedPatterns: # Allow comments to be long lines - !ruby/regexp / \# .*$/ - !ruby/regexp /^\# .*$/ Exclude: - - lib/**/*cli*.rb + - 'lib/mastodon/cli/*.rb' - db/*migrate/**/* - db/seeds/**/* +# Reason: +# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier +Lint/UselessAccessModifier: + ContextCreatingMethods: + - class_methods + +## Disable most Metrics/*Length cops +# Reason: those are often triggered and force significant refactors when this happend +# but the team feel they are not really improving the code quality. + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength +Metrics/BlockLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength +Metrics/ClassLength: + Enabled: false + +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength Metrics/MethodLength: - CountComments: false - CountAsOne: [array, heredoc] - Max: 25 # RuboCop default 10 - Exclude: - - 'lib/mastodon/*_cli.rb' - AllowedMethods: - - account_link_to - - attempt_oembed - - body_with_limit - - build_crutches - - cached_filters_for - - calculate_scores - - check_webfinger! - - clean_feeds! - - collection_items - - collection_presenter - - copy_account_notes! - - deduplicate_accounts! - - deduplicate_conversations! - - deduplicate_local_accounts! - - deduplicate_statuses! - - deduplicate_tags! - - deduplicate_users! - - discover_endpoint! - - extract_extra_uris_with_indices - - extract_hashtags_with_indices - - extract_mentions_or_lists_with_indices - - filter_from_home? - - from_elasticsearch - - handle_explicit_update! - - handle_mark_as_sensitive! - - hsl_to_rgb - - import_bookmarks! - - import_domain_blocks! - - import_relationships! - - ldap_options - - matches_time_window? - - outbox_presenter - - pam_get_user - - parallelize_with_progress - - parse_and_transform - - patch_for_forwarding! - - populate_home - - post_process_style - - preload_cache_collection_target_statuses - - privatize_media_attachments! - - provides_callback_for - - publish_media_attachments! - - relevant_account_timestamp - - remotable_attachment - - rgb_to_hsl - - rss_status_content_format - - set_fetchable_attributes! - - setup_redis_env_url - - signed_request_actor - - to_preview_card_attributes - - upgrade_storage_filesystem - - upgrade_storage_s3 - - user_settings_params - - hydrate - - cc - - self_destruct + Enabled: false +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength Metrics/ModuleLength: - CountComments: false - Max: 200 - CountAsOne: [array, heredoc] + Enabled: false +## End Disable Metrics/*Length cops + +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize +Metrics/AbcSize: + Exclude: + - 'lib/mastodon/cli/*.rb' + - db/*migrate/**/* + +# Reason: +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting +Metrics/BlockNesting: + Exclude: + - 'lib/mastodon/cli/*.rb' + +# Reason: Currently disabled in .rubocop_todo.yml +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity +Metrics/CyclomaticComplexity: + Exclude: + - lib/mastodon/cli/*.rb + - db/*migrate/**/* + +# Reason: +# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists Metrics/ParameterLists: - Max: 5 # RuboCop default 5 - CountKeywordArgs: true # RuboCop default true - MaxOptionalParameters: 3 # RuboCop default 3 - Exclude: - - app/models/concerns/account_interactions.rb - - app/services/activitypub/fetch_remote_account_service.rb - - app/services/activitypub/fetch_remote_actor_service.rb + CountKeywordArgs: false -Metrics/PerceivedComplexity: - Max: 16 # RuboCop default 8 - AllowedMethods: - - attempt_oembed - - build_crutches - - calculate_scores - - deduplicate_users! - - discover_endpoint! - - filter_from_home? - - hydrate - - patch_for_forwarding! - - process_update - - remove_orphans - - update_media_attachments! +# Reason: Prevailing style is argument file paths +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath +Rails/FilePath: + EnforcedStyle: arguments -Naming/MemoizedInstanceVariableName: - Enabled: false - -Naming/MethodParameterName: - Enabled: true - -Rails: - Enabled: true - -Rails/ApplicationController: - Enabled: false - Exclude: - - 'app/controllers/well_known/**/*.rb' - -Rails/BelongsTo: - Enabled: false - -Rails/ContentTag: - Enabled: false - -Rails/EnumHash: - Enabled: false +# Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus +Rails/HttpStatus: + EnforcedStyle: numeric +# Reason: Allowed in `tootctl` CLI code and in boot ENV checker +# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsexit Rails/Exit: Exclude: - - 'lib/mastodon/*' - - 'lib/cli.rb' + - 'config/boot.rb' + - 'lib/mastodon/cli/*.rb' -Rails/FilePath: - Enabled: false +# Reason: Some single letter camel case files shouldn't be split +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath +RSpec/FilePath: + CustomTransform: + ActivityPub: activitypub # Ignore the snake_case due to the amount of files to rename + DeepL: deepl + FetchOEmbedService: fetch_oembed_service + JsonLdHelper: jsonld_helper + OEmbedController: oembed_controller + OStatus: ostatus + NodeInfoController: nodeinfo_controller # NodeInfo isn't snake_cased for any of the instances + Exclude: + - 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder + - 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder + - 'spec/controllers/concerns/account_controller_concern_spec.rb' # Concerns describe ApplicationController and don't fit naming + - 'spec/controllers/concerns/export_controller_concern_spec.rb' + - 'spec/controllers/concerns/localized_spec.rb' + - 'spec/controllers/concerns/rate_limit_headers_spec.rb' + - 'spec/controllers/concerns/signature_verification_spec.rb' + - 'spec/controllers/concerns/user_tracking_concern_spec.rb' -Rails/HasAndBelongsToMany: - Enabled: false +# Reason: +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject +RSpec/NamedSubject: + EnforcedStyle: named_only -Rails/HasManyOrHasOneDependent: - Enabled: false +# Reason: Prevailing style choice +# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnottonot +RSpec/NotToNot: + EnforcedStyle: to_not -Rails/HelperInstanceVariable: - Enabled: false - -Rails/HttpStatus: - Enabled: false - -Rails/IndexBy: - Enabled: false - -Rails/InverseOf: - Enabled: false - -Rails/LexicallyScopedActionFilter: - Enabled: false - -Rails/OutputSafety: - Enabled: true - -Rails/RakeEnvironment: - Enabled: false - -Rails/RedundantForeignKey: - Enabled: false - -Rails/SkipsModelValidations: - Enabled: false - -Rails/UniqueValidationWithoutIndex: - Enabled: false - -Style/AccessorGrouping: - Enabled: true - -Style/AccessModifierDeclarations: - Enabled: false - -Style/ArrayCoercion: - Enabled: true - -Style/BisectedAttrAccessor: - Enabled: true - -Style/CaseLikeIf: - Enabled: false +# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus +# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus +RSpec/Rails/HttpStatus: + EnforcedStyle: numeric +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren Style/ClassAndModuleChildren: Enabled: false -Style/CollectionMethods: - Enabled: true - PreferredMethods: - find_all: 'select' - +# Reason: Classes mostly self-document with their names +# https://docs.rubocop.org/rubocop/cops_style.html#styledocumentation Style/Documentation: Enabled: false -Style/DoubleNegation: - Enabled: true - -Style/ExpandPathArguments: - Enabled: false - -Style/ExponentialNotation: - Enabled: true - -Style/FormatString: - Enabled: false - -Style/FormatStringToken: - Enabled: false - -Style/FrozenStringLiteralComment: - Enabled: true - -Style/GuardClause: - Enabled: false - -Style/HashAsLastArrayItem: - Enabled: false - -Style/HashEachMethods: - Enabled: true - -Style/HashLikeCase: - Enabled: true - -Style/HashTransformKeys: - Enabled: true - -Style/HashTransformValues: - Enabled: false - +# Reason: Enforce modern Ruby style +# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax Style/HashSyntax: - Enabled: true EnforcedStyle: ruby19_no_mixed_keys -Style/IfUnlessModifier: - Enabled: false - -Style/InverseMethods: - Enabled: false - -Style/Lambda: - Enabled: false - -Style/MutableConstant: - Enabled: false +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals +Style/NumericLiterals: + AllowedPatterns: + - \d{4}_\d{2}_\d{2}_\d{6} # For DB migration date version number readability +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#stylepercentliteraldelimiters Style/PercentLiteralDelimiters: PreferredDelimiters: '%i': '()' '%w': '()' -Style/PerlBackrefs: - AutoCorrect: false - -Style/RedundantFetchBlock: - Enabled: true - -Style/RedundantFileExtensionInRequire: - Enabled: true - -Style/RedundantRegexpCharacterClass: - Enabled: false - -Style/RedundantRegexpEscape: - Enabled: false - -Style/RedundantReturn: - Enabled: true - +# Reason: Prefer less indentation in conditional assignments +# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantbegin Style/RedundantBegin: Enabled: false -Style/RegexpLiteral: - Enabled: false - +# Reason: Overridden to reduce implicit StandardError rescues +# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror Style/RescueStandardError: - Enabled: true - -Style/SignalException: - Enabled: false - -Style/SlicingWithRange: - Enabled: true + EnforcedStyle: implicit +# Reason: Originally disabled for CodeClimate, and no config consensus has been found +# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray Style/SymbolArray: Enabled: false +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainarrayliteral Style/TrailingCommaInArrayLiteral: EnforcedStyleForMultiline: 'comma' +# Reason: +# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' -Style/UnpackFirst: - Enabled: false - -RSpec/ScatteredSetup: - Enabled: false -RSpec/ImplicitExpect: - Enabled: false -RSpec/NamedSubject: - Enabled: false -RSpec/DescribeClass: - Enabled: false -RSpec/LetSetup: - Enabled: false +Style/MiddleDot: + Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000000..c1d580e515 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,1452 @@ +# This configuration was generated by +# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` +# using RuboCop version 1.52.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. +# Include: **/*.gemfile, **/Gemfile, **/gems.rb +Bundler/OrderedGems: + Exclude: + - 'Gemfile' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_argument, with_fixed_indentation +Layout/ArgumentAlignment: + Exclude: + - 'config/initializers/cors.rb' + - 'config/initializers/session_store.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. +# SupportedHashRocketStyles: key, separator, table +# SupportedColonStyles: key, separator, table +# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit +Layout/HashAlignment: + Exclude: + - 'config/boot.rb' + - 'config/environments/production.rb' + - 'config/initializers/rack_attack.rb' + - 'config/routes.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment. +Layout/LeadingCommentSpace: + Exclude: + - 'config/application.rb' + - 'config/initializers/omniauth.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_no_space, require_space +Layout/SpaceInLambdaLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/content_security_policy.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. +Lint/AmbiguousBlockAssociation: + Exclude: + - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb' + - 'spec/services/activitypub/process_status_update_service_spec.rb' + - 'spec/services/post_status_service_spec.rb' + +# Configuration parameters: AllowComments, AllowEmptyLambdas. +Lint/EmptyBlock: + Exclude: + - 'spec/controllers/api/v2/search_controller_spec.rb' + - 'spec/fabricators/access_token_fabricator.rb' + - 'spec/fabricators/conversation_fabricator.rb' + - 'spec/fabricators/system_key_fabricator.rb' + - 'spec/helpers/admin/action_logs_helper_spec.rb' + - 'spec/lib/activitypub/adapter_spec.rb' + - 'spec/models/account_alias_spec.rb' + - 'spec/models/account_deletion_request_spec.rb' + - 'spec/models/account_moderation_note_spec.rb' + - 'spec/models/announcement_mute_spec.rb' + - 'spec/models/announcement_reaction_spec.rb' + - 'spec/models/announcement_spec.rb' + - 'spec/models/backup_spec.rb' + - 'spec/models/conversation_mute_spec.rb' + - 'spec/models/custom_filter_keyword_spec.rb' + - 'spec/models/custom_filter_spec.rb' + - 'spec/models/device_spec.rb' + - 'spec/models/encrypted_message_spec.rb' + - 'spec/models/featured_tag_spec.rb' + - 'spec/models/follow_recommendation_suppression_spec.rb' + - 'spec/models/list_account_spec.rb' + - 'spec/models/list_spec.rb' + - 'spec/models/login_activity_spec.rb' + - 'spec/models/mute_spec.rb' + - 'spec/models/preview_card_spec.rb' + - 'spec/models/preview_card_trend_spec.rb' + - 'spec/models/relay_spec.rb' + - 'spec/models/scheduled_status_spec.rb' + - 'spec/models/status_stat_spec.rb' + - 'spec/models/status_trend_spec.rb' + - 'spec/models/system_key_spec.rb' + - 'spec/models/tag_follow_spec.rb' + - 'spec/models/unavailable_domain_spec.rb' + - 'spec/models/user_invite_request_spec.rb' + - 'spec/models/user_role_spec.rb' + - 'spec/models/web/setting_spec.rb' + +Lint/NonLocalExitFromIterator: + Exclude: + - 'app/helpers/jsonld_helper.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/OrAssignmentToConstant: + Exclude: + - 'lib/sanitize_ext/sanitize_config.rb' + +# This cop supports safe autocorrection (--autocorrect). +Lint/SendWithMixinArgument: + Exclude: + - 'config/application.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. +Lint/UnusedBlockArgument: + Exclude: + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/doorkeeper.rb' + - 'config/initializers/paperclip.rb' + - 'config/initializers/simple_form.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Lint/UselessAssignment: + Exclude: + - 'app/services/activitypub/process_status_update_service.rb' + - 'config/initializers/omniauth.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' + - 'spec/controllers/api/v1/bookmarks_controller_spec.rb' + - 'spec/controllers/api/v1/favourites_controller_spec.rb' + - 'spec/controllers/concerns/account_controller_concern_spec.rb' + - 'spec/helpers/jsonld_helper_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/domain_block_spec.rb' + - 'spec/models/status_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/models/webauthn_credentials_spec.rb' + - 'spec/services/account_search_service_spec.rb' + - 'spec/services/post_status_service_spec.rb' + - 'spec/services/precompute_feed_service_spec.rb' + - 'spec/services/resolve_url_service_spec.rb' + - 'spec/views/statuses/show.html.haml_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: CheckForMethodsWithNoSideEffects. +Lint/Void: + Exclude: + - 'spec/services/resolve_account_service_spec.rb' + +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 150 + Exclude: + - 'app/serializers/initial_state_serializer.rb' + +# Configuration parameters: CountBlocks, Max. +Metrics/BlockNesting: + Exclude: + - 'lib/tasks/mastodon.rake' + +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/CyclomaticComplexity: + Max: 25 + +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/PerceivedComplexity: + Max: 27 + +Naming/AccessorMethodName: + Exclude: + - 'app/controllers/auth/sessions_controller.rb' + +# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. +# CheckDefinitionPathHierarchyRoots: lib, spec, test, src +# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS +Naming/FileName: + Exclude: + - 'config/locales/sr-Latn.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyleForLeadingUnderscores. +# SupportedStylesForLeadingUnderscores: disallowed, required, optional +Naming/MemoizedInstanceVariableName: + Exclude: + - 'app/controllers/api/v1/bookmarks_controller.rb' + - 'app/controllers/api/v1/favourites_controller.rb' + - 'app/controllers/concerns/rate_limit_headers.rb' + - 'app/lib/activitypub/activity.rb' + - 'app/services/resolve_url_service.rb' + - 'app/services/search_service.rb' + - 'config/initializers/rack_attack.rb' + +# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. +# SupportedStyles: snake_case, normalcase, non_integer +# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 +Naming/VariableNumber: + Exclude: + - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20180514140000_revert_index_change_on_statuses_for_api_v1_accounts_account_id_statuses.rb' + - 'db/migrate/20190820003045_update_statuses_index.rb' + - 'db/migrate/20190823221802_add_local_index_to_statuses.rb' + - 'db/migrate/20200119112504_add_public_index_to_statuses.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/domain_block_spec.rb' + - 'spec/models/user_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Performance/UnfreezeString: + Exclude: + - 'app/lib/rss/builder.rb' + - 'app/lib/text_formatter.rb' + - 'app/validators/status_length_validator.rb' + - 'lib/tasks/mastodon.rake' + +RSpec/AnyInstance: + Exclude: + - 'spec/controllers/activitypub/inboxes_controller_spec.rb' + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/controllers/admin/resets_controller_spec.rb' + - 'spec/controllers/admin/settings/branding_controller_spec.rb' + - 'spec/controllers/api/v1/media_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/recovery_codes_controller_spec.rb' + - 'spec/lib/request_spec.rb' + - 'spec/lib/status_filter_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/setting_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/validators/blacklisted_email_validator_spec.rb' + - 'spec/validators/follow_limit_validator_spec.rb' + - 'spec/workers/activitypub/delivery_worker_spec.rb' + - 'spec/workers/web/push_notification_worker_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +RSpec/EmptyExampleGroup: + Exclude: + - 'spec/helpers/admin/action_logs_helper_spec.rb' + - 'spec/models/account_alias_spec.rb' + - 'spec/models/account_deletion_request_spec.rb' + - 'spec/models/account_moderation_note_spec.rb' + - 'spec/models/announcement_mute_spec.rb' + - 'spec/models/announcement_reaction_spec.rb' + - 'spec/models/announcement_spec.rb' + - 'spec/models/backup_spec.rb' + - 'spec/models/conversation_mute_spec.rb' + - 'spec/models/custom_filter_keyword_spec.rb' + - 'spec/models/custom_filter_spec.rb' + - 'spec/models/device_spec.rb' + - 'spec/models/encrypted_message_spec.rb' + - 'spec/models/featured_tag_spec.rb' + - 'spec/models/follow_recommendation_suppression_spec.rb' + - 'spec/models/list_account_spec.rb' + - 'spec/models/list_spec.rb' + - 'spec/models/login_activity_spec.rb' + - 'spec/models/mute_spec.rb' + - 'spec/models/preview_card_spec.rb' + - 'spec/models/preview_card_trend_spec.rb' + - 'spec/models/relay_spec.rb' + - 'spec/models/scheduled_status_spec.rb' + - 'spec/models/status_stat_spec.rb' + - 'spec/models/status_trend_spec.rb' + - 'spec/models/system_key_spec.rb' + - 'spec/models/tag_follow_spec.rb' + - 'spec/models/unavailable_domain_spec.rb' + - 'spec/models/user_invite_request_spec.rb' + - 'spec/models/web/setting_spec.rb' + - 'spec/services/unmute_service_spec.rb' + +# Configuration parameters: CountAsOne. +RSpec/ExampleLength: + Max: 22 + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: implicit, each, example +RSpec/HookArgument: + Exclude: + - 'spec/controllers/api/v1/streaming_controller_spec.rb' + - 'spec/controllers/well_known/webfinger_controller_spec.rb' + - 'spec/helpers/instance_helper_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/rails_helper.rb' + - 'spec/serializers/activitypub/note_serializer_spec.rb' + - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' + - 'spec/services/import_service_spec.rb' + - 'spec/spec_helper.rb' + +# Configuration parameters: AssignmentOnly. +RSpec/InstanceVariable: + Exclude: + - 'spec/controllers/api/v1/streaming_controller_spec.rb' + - 'spec/controllers/auth/confirmations_controller_spec.rb' + - 'spec/controllers/auth/passwords_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/concerns/export_controller_concern_spec.rb' + - 'spec/controllers/home_controller_spec.rb' + - 'spec/controllers/settings/two_factor_authentication/webauthn_credentials_controller_spec.rb' + - 'spec/controllers/statuses_cleanup_controller_spec.rb' + - 'spec/models/concerns/account_finder_concern_spec.rb' + - 'spec/models/concerns/account_interactions_spec.rb' + - 'spec/models/public_feed_spec.rb' + - 'spec/serializers/activitypub/note_serializer_spec.rb' + - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' + - 'spec/services/remove_status_service_spec.rb' + - 'spec/services/search_service_spec.rb' + - 'spec/services/unblock_domain_service_spec.rb' + +RSpec/LetSetup: + Exclude: + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/controllers/admin/action_logs_controller_spec.rb' + - 'spec/controllers/admin/instances_controller_spec.rb' + - 'spec/controllers/admin/reports/actions_controller_spec.rb' + - 'spec/controllers/admin/statuses_controller_spec.rb' + - 'spec/controllers/api/v1/accounts/statuses_controller_spec.rb' + - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' + - 'spec/controllers/api/v1/filters_controller_spec.rb' + - 'spec/controllers/api/v1/followed_tags_controller_spec.rb' + - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' + - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' + - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' + - 'spec/controllers/api/v2/filters_controller_spec.rb' + - 'spec/controllers/auth/confirmations_controller_spec.rb' + - 'spec/controllers/auth/passwords_controller_spec.rb' + - 'spec/controllers/auth/sessions_controller_spec.rb' + - 'spec/controllers/follower_accounts_controller_spec.rb' + - 'spec/controllers/following_accounts_controller_spec.rb' + - 'spec/controllers/oauth/authorized_applications_controller_spec.rb' + - 'spec/controllers/oauth/tokens_controller_spec.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' + - 'spec/lib/activitypub/activity/delete_spec.rb' + - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/account_statuses_cleanup_policy_spec.rb' + - 'spec/models/canonical_email_block_spec.rb' + - 'spec/models/status_spec.rb' + - 'spec/models/user_spec.rb' + - 'spec/services/account_statuses_cleanup_service_spec.rb' + - 'spec/services/activitypub/fetch_featured_collection_service_spec.rb' + - 'spec/services/activitypub/fetch_remote_status_service_spec.rb' + - 'spec/services/activitypub/process_account_service_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/services/batched_remove_status_service_spec.rb' + - 'spec/services/block_domain_service_spec.rb' + - 'spec/services/bulk_import_service_spec.rb' + - 'spec/services/delete_account_service_spec.rb' + - 'spec/services/import_service_spec.rb' + - 'spec/services/notify_service_spec.rb' + - 'spec/services/remove_status_service_spec.rb' + - 'spec/services/report_service_spec.rb' + - 'spec/services/resolve_account_service_spec.rb' + - 'spec/services/suspend_account_service_spec.rb' + - 'spec/services/unallow_domain_service_spec.rb' + - 'spec/services/unsuspend_account_service_spec.rb' + - 'spec/workers/scheduler/user_cleanup_scheduler_spec.rb' + +RSpec/MessageChain: + Exclude: + - 'spec/controllers/api/v1/media_controller_spec.rb' + - 'spec/models/concerns/remotable_spec.rb' + - 'spec/models/session_activation_spec.rb' + - 'spec/models/setting_spec.rb' + +# Configuration parameters: EnforcedStyle. +# SupportedStyles: have_received, receive +RSpec/MessageSpies: + Exclude: + - 'spec/controllers/admin/accounts_controller_spec.rb' + - 'spec/controllers/api/base_controller_spec.rb' + - 'spec/controllers/auth/registrations_controller_spec.rb' + - 'spec/helpers/admin/account_moderation_notes_helper_spec.rb' + - 'spec/helpers/application_helper_spec.rb' + - 'spec/lib/status_finder_spec.rb' + - 'spec/lib/webfinger_resource_spec.rb' + - 'spec/models/admin/account_action_spec.rb' + - 'spec/models/concerns/remotable_spec.rb' + - 'spec/models/follow_request_spec.rb' + - 'spec/models/identity_spec.rb' + - 'spec/models/session_activation_spec.rb' + - 'spec/models/setting_spec.rb' + - 'spec/services/activitypub/fetch_replies_service_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/spec_helper.rb' + - 'spec/validators/status_length_validator_spec.rb' + +RSpec/MultipleExpectations: + Max: 8 + +# Configuration parameters: AllowSubject. +RSpec/MultipleMemoizedHelpers: + Max: 21 + +# Configuration parameters: AllowedGroups. +RSpec/NestedGroups: + Max: 6 + +RSpec/PendingWithoutReason: + Exclude: + - 'spec/models/account_spec.rb' + +RSpec/StubbedMock: + Exclude: + - 'spec/controllers/api/base_controller_spec.rb' + - 'spec/controllers/api/v1/media_controller_spec.rb' + - 'spec/controllers/auth/registrations_controller_spec.rb' + - 'spec/helpers/application_helper_spec.rb' + - 'spec/lib/status_filter_spec.rb' + - 'spec/lib/status_finder_spec.rb' + - 'spec/lib/webfinger_resource_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + +RSpec/SubjectDeclaration: + Exclude: + - 'spec/controllers/admin/domain_blocks_controller_spec.rb' + - 'spec/models/account_migration_spec.rb' + - 'spec/models/account_spec.rb' + - 'spec/models/relationship_filter_spec.rb' + - 'spec/models/user_role_spec.rb' + - 'spec/policies/account_moderation_note_policy_spec.rb' + - 'spec/policies/account_policy_spec.rb' + - 'spec/policies/backup_policy_spec.rb' + - 'spec/policies/custom_emoji_policy_spec.rb' + - 'spec/policies/domain_block_policy_spec.rb' + - 'spec/policies/email_domain_block_policy_spec.rb' + - 'spec/policies/instance_policy_spec.rb' + - 'spec/policies/invite_policy_spec.rb' + - 'spec/policies/relay_policy_spec.rb' + - 'spec/policies/report_note_policy_spec.rb' + - 'spec/policies/report_policy_spec.rb' + - 'spec/policies/settings_policy_spec.rb' + - 'spec/policies/tag_policy_spec.rb' + - 'spec/policies/user_policy_spec.rb' + - 'spec/services/activitypub/process_account_service_spec.rb' + +RSpec/SubjectStub: + Exclude: + - 'spec/services/unallow_domain_service_spec.rb' + - 'spec/validators/blacklisted_email_validator_spec.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/ApplicationController: + Exclude: + - 'app/controllers/health_controller.rb' + +# Configuration parameters: Database, Include. +# SupportedDatabases: mysql, postgresql +# Include: db/migrate/*.rb +Rails/BulkChangeTable: + Exclude: + - 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb' + - 'db/migrate/20160223162837_add_metadata_to_statuses.rb' + - 'db/migrate/20160305115639_add_devise_to_users.rb' + - 'db/migrate/20160314164231_add_owner_to_application.rb' + - 'db/migrate/20160926213048_remove_owner_from_application.rb' + - 'db/migrate/20161003142332_add_confirmable_to_users.rb' + - 'db/migrate/20170112154826_migrate_settings.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb' + - 'db/migrate/20170330021336_add_counter_caches.rb' + - 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb' + - 'db/migrate/20170427011934_re_add_owner_to_application.rb' + - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb' + - 'db/migrate/20170624134742_add_description_to_session_activations.rb' + - 'db/migrate/20170718211102_add_activitypub_to_accounts.rb' + - 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb' + - 'db/migrate/20180812123222_change_relays_enabled.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.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/20190901035623_add_max_score_to_tags.rb' + - 'db/migrate/20200417125749_add_storage_schema_version.rb' + - 'db/migrate/20200608113046_add_sign_in_token_to_users.rb' + - 'db/migrate/20211112011713_add_language_to_preview_cards.rb' + - 'db/migrate/20211231080958_add_category_to_reports.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.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/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' + - 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb' + +# Configuration parameters: Include. +# Include: db/migrate/*.rb +Rails/CreateTableWithTimestamps: + Exclude: + - 'db/migrate/20170508230434_create_conversation_mutes.rb' + - 'db/migrate/20170823162448_create_status_pins.rb' + - 'db/migrate/20171116161857_create_list_accounts.rb' + - 'db/migrate/20180929222014_create_account_conversations.rb' + - 'db/migrate/20181007025445_create_pghero_space_stats.rb' + - 'db/migrate/20190103124649_create_scheduled_statuses.rb' + - 'db/migrate/20220824233535_create_status_trends.rb' + - 'db/migrate/20221006061337_create_preview_card_trends.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Severity. +Rails/DuplicateAssociation: + Exclude: + - 'app/serializers/activitypub/collection_serializer.rb' + - 'app/serializers/activitypub/note_serializer.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasAndBelongsToMany: + Exclude: + - 'app/models/concerns/account_associations.rb' + - 'app/models/preview_card.rb' + - 'app/models/status.rb' + - 'app/models/tag.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/HasManyOrHasOneDependent: + Exclude: + - 'app/models/concerns/account_counters.rb' + - 'app/models/conversation.rb' + - 'app/models/custom_emoji.rb' + - 'app/models/custom_emoji_category.rb' + - 'app/models/domain_block.rb' + - 'app/models/invite.rb' + - 'app/models/status.rb' + - 'app/models/user.rb' + - 'app/models/web/push_subscription.rb' + +Rails/I18nLocaleTexts: + Exclude: + - 'lib/tasks/mastodon.rake' + - 'spec/helpers/flashes_helper_spec.rb' + +# Configuration parameters: Include. +# Include: app/controllers/**/*.rb, app/mailers/**/*.rb +Rails/LexicallyScopedActionFilter: + Exclude: + - 'app/controllers/auth/passwords_controller.rb' + - 'app/controllers/auth/registrations_controller.rb' + - 'app/controllers/auth/sessions_controller.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/NegateInclude: + Exclude: + - 'app/controllers/concerns/signature_verification.rb' + - 'app/helpers/jsonld_helper.rb' + - 'app/lib/activitypub/activity/create.rb' + - 'app/lib/activitypub/activity/move.rb' + - 'app/lib/feed_manager.rb' + - 'app/lib/link_details_extractor.rb' + - 'app/models/concerns/attachmentable.rb' + - 'app/models/concerns/remotable.rb' + - 'app/models/custom_filter.rb' + - 'app/services/activitypub/process_status_update_service.rb' + - 'app/services/fetch_link_card_service.rb' + - 'app/services/search_service.rb' + - 'app/workers/web/push_notification_worker.rb' + - 'lib/paperclip/color_extractor.rb' + +Rails/OutputSafety: + Exclude: + - 'config/initializers/simple_form.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Include. +# Include: **/Rakefile, **/*.rake +Rails/RakeEnvironment: + Exclude: + - 'lib/tasks/auto_annotate_models.rake' + - 'lib/tasks/db.rake' + - 'lib/tasks/emojis.rake' + - 'lib/tasks/mastodon.rake' + - 'lib/tasks/repo.rake' + - 'lib/tasks/statistics.rake' + +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ReversibleMigration: + Exclude: + - 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb' + - 'db/migrate/20161122163057_remove_unneeded_indexes.rb' + - 'db/migrate/20170205175257_remove_devices.rb' + - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb' + - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb' + - 'db/migrate/20170609145826_remove_default_language_from_statuses.rb' + - 'db/migrate/20170711225116_fix_null_booleans.rb' + - 'db/migrate/20171129172043_add_index_on_stream_entries.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/20180617162849_remove_unused_indexes.rb' + - 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb' + +# Configuration parameters: ForbiddenMethods, AllowedMethods. +# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all +Rails/SkipsModelValidations: + Exclude: + - 'app/controllers/admin/invites_controller.rb' + - 'app/controllers/concerns/session_tracking_concern.rb' + - 'app/models/concerns/account_merging.rb' + - 'app/models/concerns/expireable.rb' + - 'app/models/status.rb' + - 'app/models/trends/links.rb' + - 'app/models/trends/preview_card_batch.rb' + - 'app/models/trends/preview_card_provider_batch.rb' + - 'app/models/trends/status_batch.rb' + - 'app/models/trends/statuses.rb' + - 'app/models/trends/tag_batch.rb' + - 'app/models/trends/tags.rb' + - 'app/models/user.rb' + - 'app/services/activitypub/process_status_update_service.rb' + - 'app/services/approve_appeal_service.rb' + - 'app/services/block_domain_service.rb' + - 'app/services/delete_account_service.rb' + - 'app/services/process_mentions_service.rb' + - 'app/services/unallow_domain_service.rb' + - 'app/services/unblock_domain_service.rb' + - 'app/services/update_status_service.rb' + - 'app/workers/activitypub/post_upgrade_worker.rb' + - 'app/workers/move_worker.rb' + - 'app/workers/scheduler/ip_cleanup_scheduler.rb' + - 'app/workers/scheduler/scheduled_statuses_scheduler.rb' + - 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb' + - 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170304202101_add_type_to_media_attachments.rb' + - 'db/migrate/20180528141303_fix_accounts_unique_index.rb' + - 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb' + - 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb' + - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' + - 'db/migrate/20191007013357_update_pt_locales.rb' + - 'db/migrate/20220316233212_update_kurdish_locales.rb' + - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' + - 'db/post_migrate/20200917193528_migrate_notifications_type.rb' + - 'db/post_migrate/20201017234926_fill_account_suspension_origin.rb' + - 'db/post_migrate/20220617202502_migrate_roles.rb' + - 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb' + - 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb' + - 'lib/mastodon/cli/accounts.rb' + - 'lib/mastodon/cli/main.rb' + - 'lib/mastodon/cli/maintenance.rb' + - 'spec/controllers/api/v1/admin/accounts_controller_spec.rb' + - 'spec/lib/activitypub/activity/follow_spec.rb' + - 'spec/services/follow_service_spec.rb' + - 'spec/services/update_account_service_spec.rb' + +# Configuration parameters: Include. +# Include: db/**/*.rb +Rails/ThreeStateBooleanColumn: + Exclude: + - 'db/migrate/20160325130944_add_admin_to_users.rb' + - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb' + - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb' + - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' + - 'db/migrate/20170209184350_add_reply_to_statuses.rb' + - 'db/migrate/20170330163835_create_imports.rb' + - 'db/migrate/20170905165803_add_local_to_statuses.rb' + - 'db/migrate/20171210213213_add_local_only_flag_to_statuses.rb' + - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb' + - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb' + - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' + - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb' + - 'db/migrate/20200309150742_add_forwarded_to_reports.rb' + - 'db/migrate/20210609202149_create_login_activities.rb' + - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb' + - 'db/migrate/20211031031021_create_preview_card_providers.rb' + - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb' + - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' + - 'db/migrate/20220202200926_add_trendable_to_statuses.rb' + - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/UniqueValidationWithoutIndex: + Exclude: + - 'app/models/account_alias.rb' + - 'app/models/custom_filter_status.rb' + - 'app/models/identity.rb' + - 'app/models/webauthn_credential.rb' + +# Configuration parameters: Include. +# Include: app/models/**/*.rb +Rails/UnusedIgnoredColumns: + Exclude: + - 'app/models/account.rb' + - 'app/models/account_stat.rb' + - 'app/models/admin/action_log.rb' + - 'app/models/custom_filter.rb' + - 'app/models/email_domain_block.rb' + - 'app/models/report.rb' + - 'app/models/status_edit.rb' + - 'app/models/user.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: exists, where +Rails/WhereExists: + Exclude: + - 'app/controllers/activitypub/inboxes_controller.rb' + - 'app/controllers/admin/email_domain_blocks_controller.rb' + - 'app/controllers/auth/registrations_controller.rb' + - 'app/lib/activitypub/activity/create.rb' + - 'app/lib/delivery_failure_tracker.rb' + - 'app/lib/feed_manager.rb' + - 'app/lib/status_cache_hydrator.rb' + - 'app/lib/suspicious_sign_in_detector.rb' + - 'app/models/concerns/account_interactions.rb' + - 'app/models/featured_tag.rb' + - 'app/models/poll.rb' + - 'app/models/session_activation.rb' + - 'app/models/status.rb' + - 'app/models/user.rb' + - 'app/policies/status_policy.rb' + - 'app/serializers/rest/announcement_serializer.rb' + - 'app/serializers/rest/tag_serializer.rb' + - 'app/services/activitypub/fetch_remote_status_service.rb' + - 'app/services/app_sign_up_service.rb' + - 'app/services/vote_service.rb' + - 'app/validators/reaction_validator.rb' + - 'app/validators/vote_validator.rb' + - 'app/workers/move_worker.rb' + - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb' + - 'lib/tasks/tests.rake' + - 'spec/models/account_spec.rb' + - 'spec/services/activitypub/process_collection_service_spec.rb' + - 'spec/services/purge_domain_service_spec.rb' + - 'spec/services/unallow_domain_service_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowOnConstant, AllowOnSelfClass. +Style/CaseEquality: + Exclude: + - 'config/initializers/trusted_proxies.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedMethods, AllowedPatterns. +# AllowedMethods: ==, equal?, eql? +Style/ClassEqualityComparison: + Exclude: + - 'app/helpers/jsonld_helper.rb' + - 'app/serializers/activitypub/outbox_serializer.rb' + +Style/ClassVars: + Exclude: + - 'config/initializers/devise.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/CombinableLoops: + Exclude: + - 'app/models/form/custom_emoji_batch.rb' + - 'app/models/form/ip_block_batch.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowedVars. +Style/FetchEnvVar: + Exclude: + - 'app/lib/redis_configuration.rb' + - 'app/lib/translation_service.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + - 'config/initializers/2_whitelist_mode.rb' + - 'config/initializers/blacklists.rb' + - 'config/initializers/cache_buster.rb' + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/omniauth.rb' + - 'config/initializers/paperclip.rb' + - 'config/initializers/vapid.rb' + - 'lib/mastodon/premailer_webpack_strategy.rb' + - 'lib/mastodon/redis_config.rb' + - 'lib/tasks/repo.rake' + - 'spec/features/profile_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. +# SupportedStyles: annotated, template, unannotated +# AllowedMethods: redirect +Style/FormatStringToken: + Exclude: + - 'app/models/privacy_policy.rb' + - 'config/initializers/devise.rb' + - 'lib/paperclip/color_extractor.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: always, always_true, never +Style/FrozenStringLiteralComment: + Exclude: + - 'app/views/accounts/show.rss.ruby' + - 'app/views/tags/show.rss.ruby' + - 'app/views/well_known/host_meta/show.xml.ruby' + - 'config/application.rb' + - 'config/boot.rb' + - 'config/environment.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + - 'config/environments/test.rb' + - 'config/initializers/0_post_deployment_migrations.rb' + - 'config/initializers/active_model_serializers.rb' + - 'config/initializers/application_controller_renderer.rb' + - 'config/initializers/assets.rb' + - 'config/initializers/backtrace_silencers.rb' + - 'config/initializers/cache_logging.rb' + - 'config/initializers/chewy.rb' + - 'config/initializers/content_security_policy.rb' + - 'config/initializers/cookies_serializer.rb' + - 'config/initializers/cors.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/doorkeeper.rb' + - 'config/initializers/fast_blank.rb' + - 'config/initializers/ffmpeg.rb' + - 'config/initializers/filter_parameter_logging.rb' + - 'config/initializers/http_client_proxy.rb' + - 'config/initializers/httplog.rb' + - 'config/initializers/inflections.rb' + - 'config/initializers/mail_delivery_job.rb' + - 'config/initializers/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). +Style/GlobalStdStream: + Exclude: + - 'config/boot.rb' + - 'config/environments/development.rb' + - 'config/environments/production.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. +Style/GuardClause: + Exclude: + - 'app/controllers/admin/confirmations_controller.rb' + - 'app/controllers/auth/confirmations_controller.rb' + - 'app/controllers/auth/passwords_controller.rb' + - 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb' + - 'app/lib/activitypub/activity/block.rb' + - 'app/lib/request.rb' + - 'app/lib/request_pool.rb' + - 'app/lib/webfinger.rb' + - 'app/lib/webfinger_resource.rb' + - 'app/models/concerns/account_counters.rb' + - 'app/models/concerns/ldap_authenticable.rb' + - 'app/models/tag.rb' + - 'app/models/user.rb' + - 'app/services/fan_out_on_write_service.rb' + - 'app/services/post_status_service.rb' + - 'app/services/process_hashtags_service.rb' + - 'app/workers/move_worker.rb' + - 'app/workers/redownload_avatar_worker.rb' + - 'app/workers/redownload_header_worker.rb' + - 'app/workers/redownload_media_worker.rb' + - 'app/workers/remote_account_refresh_worker.rb' + - 'config/initializers/devise.rb' + - 'db/migrate/20170901141119_truncate_preview_cards.rb' + - 'db/post_migrate/20220704024901_migrate_settings_to_user_roles.rb' + - 'lib/devise/two_factor_ldap_authenticatable.rb' + - 'lib/devise/two_factor_pam_authenticatable.rb' + - 'lib/mastodon/cli/accounts.rb' + - 'lib/mastodon/cli/maintenance.rb' + - 'lib/mastodon/cli/media.rb' + - 'lib/paperclip/attachment_extensions.rb' + - 'lib/tasks/repo.rake' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: braces, no_braces +Style/HashAsLastArrayItem: + Exclude: + - 'app/controllers/admin/statuses_controller.rb' + - 'app/controllers/api/v1/statuses_controller.rb' + - 'app/models/concerns/account_counters.rb' + - 'app/models/concerns/status_threading_concern.rb' + - 'app/models/status.rb' + - 'app/services/batched_remove_status_service.rb' + - 'app/services/notify_service.rb' + - 'db/migrate/20181024224956_migrate_account_conversations.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/HashTransformValues: + Exclude: + - 'app/serializers/rest/web_push_subscription_serializer.rb' + - 'app/services/import_service.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/ffmpeg.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: InverseMethods, InverseBlocks. +Style/InverseMethods: + Exclude: + - 'app/models/custom_filter.rb' + - 'app/services/update_account_service.rb' + - 'spec/controllers/activitypub/replies_controller_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: line_count_dependent, lambda, literal +Style/Lambda: + Exclude: + - 'config/initializers/simple_form.rb' + - 'config/routes.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/MapToHash: + Exclude: + - 'app/models/status.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: literals, strict +Style/MutableConstant: + Exclude: + - 'app/models/tag.rb' + - 'app/services/delete_account_service.rb' + - 'config/initializers/twitter_regex.rb' + - 'lib/mastodon/migration_warning.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/NilLambda: + Exclude: + - 'config/initializers/paperclip.rb' + +# Configuration parameters: AllowedMethods. +# AllowedMethods: respond_to_missing? +Style/OptionalBooleanParameter: + Exclude: + - 'app/helpers/admin/account_moderation_notes_helper.rb' + - 'app/helpers/jsonld_helper.rb' + - 'app/lib/admin/system_check/message.rb' + - 'app/lib/request.rb' + - 'app/lib/webfinger.rb' + - 'app/services/block_domain_service.rb' + - 'app/services/fetch_resource_service.rb' + - 'app/workers/domain_block_worker.rb' + - 'app/workers/unfollow_follow_worker.rb' + - 'lib/mastodon/redis_config.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: PreferredDelimiters. +Style/PercentLiteralDelimiters: + Exclude: + - 'config/deploy.rb' + - 'config/initializers/doorkeeper.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: short, verbose +Style/PreferredHashMethods: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantConstantBase: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/sidekiq.rb' + - 'config/locales/sr-Latn.rb' + - 'config/locales/sr.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: SafeForConstants. +Style/RedundantFetchBlock: + Exclude: + - 'config/initializers/1_hosts.rb' + - 'config/initializers/chewy.rb' + - 'config/initializers/devise.rb' + - 'config/initializers/paperclip.rb' + - 'config/puma.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. +# AllowedMethods: present?, blank?, presence, try, try! +Style/SafeNavigation: + Exclude: + - 'app/models/concerns/account_finder_concern.rb' + - 'app/models/status.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowAsExpressionSeparator. +Style/Semicolon: + Exclude: + - 'spec/services/activitypub/process_status_update_service_spec.rb' + - 'spec/validators/blacklisted_email_validator_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: only_raise, only_fail, semantic +Style/SignalException: + Exclude: + - 'lib/devise/two_factor_ldap_authenticatable.rb' + - 'lib/devise/two_factor_pam_authenticatable.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/SingleArgumentDig: + Exclude: + - 'lib/webpacker/manifest_extensions.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/SlicingWithRange: + Exclude: + - 'app/lib/emoji_formatter.rb' + - 'app/lib/text_formatter.rb' + - 'app/models/account_alias.rb' + - 'app/models/domain_block.rb' + - 'app/models/email_domain_block.rb' + - 'app/models/preview_card_provider.rb' + - 'app/validators/status_length_validator.rb' + - 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb' + - 'lib/active_record/batches.rb' + - 'lib/mastodon/premailer_webpack_strategy.rb' + - 'lib/tasks/repo.rake' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle. +# SupportedStyles: require_parentheses, require_no_parentheses +Style/StabbyLambdaParentheses: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/content_security_policy.rb' + +# This cop supports safe autocorrection (--autocorrect). +Style/StderrPuts: + Exclude: + - 'config/boot.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'config/environments/production.rb' + - 'config/initializers/backtrace_silencers.rb' + - 'config/initializers/http_client_proxy.rb' + - 'config/initializers/rack_attack.rb' + - 'config/initializers/webauthn.rb' + - 'config/routes.rb' + +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. +# AllowedMethods: define_method, mail, respond_to +Style/SymbolProc: + Exclude: + - 'config/initializers/omniauth.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, AllowSafeAssignment. +# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex +Style/TernaryParentheses: + Exclude: + - 'config/environments/development.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInArguments: + Exclude: + - 'config/initializers/paperclip.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyleForMultiline. +# SupportedStylesForMultiline: comma, consistent_comma, no_comma +Style/TrailingCommaInHashLiteral: + Exclude: + - 'config/environments/production.rb' + - 'config/environments/test.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, MinSize, WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + Exclude: + - 'app/helpers/languages_helper.rb' + - 'config/initializers/cors.rb' + - 'spec/controllers/settings/imports_controller_spec.rb' + - 'spec/models/form/import_spec.rb' + +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 701 diff --git a/.ruby-version b/.ruby-version index b0f2dcb32f..be94e6f53d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.4 +3.2.2 diff --git a/.yarnclean b/.yarnclean index 0cc2b50d7b..21eb734a6c 100644 --- a/.yarnclean +++ b/.yarnclean @@ -44,3 +44,6 @@ Gruntfile.js # for specific ignore !.svgo.yml !sass-lint/**/*.yml + +# breaks lint-staged or generally anything using https://github.com/eemeli/yaml/issues/384 +!**/yaml/dist/**/doc diff --git a/Aptfile b/Aptfile index 8f5bb72a25..5e033f1365 100644 --- a/Aptfile +++ b/Aptfile @@ -1,4 +1,5 @@ ffmpeg +libopenblas0-pthread libpq-dev libxdamage1 libxfixes3 diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ad9e5fd9..91a2c48a1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,289 @@ -Changelog -========= +# Changelog All notable changes to this project will be documented in this file. +## [4.1.2] - 2023-04-04 + +### Fixed + +- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377)) +- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302)) +- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200)) +- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337)) + +### Security + +- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24334)) +- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379)) + +## [4.1.1] - 2023-03-16 + +### Added + +- Add redirection from paths with url-encoded `@` to their decoded form ([thijskh](https://github.com/mastodon/mastodon/pull/23593)) +- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749)) +- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597)) +- Add support for refreshing many accounts at once with `tootctl accounts refresh` ([9p4](https://github.com/mastodon/mastodon/pull/23304)) +- Add confirmation modal when clicking to edit a post with a non-empty compose form ([PauloVilarinho](https://github.com/mastodon/mastodon/pull/23936)) +- Add support for the HAproxy PROXY protocol through the `PROXY_PROTO_V1` environment variable ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24064)) +- Add `SENDFILE_HEADER` environment variable ([Gargron](https://github.com/mastodon/mastodon/pull/24123)) +- Add cache headers to static files served through Rails ([Gargron](https://github.com/mastodon/mastodon/pull/24120)) + +### Changed + +- Increase contrast of upload progress bar background ([toolmantim](https://github.com/mastodon/mastodon/pull/23836)) +- Change post auto-deletion throttling constants to better scale with server size ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23320)) +- Change order of bookmark and favourite sidebar entries in single-column UI for consistency ([TerryGarcia](https://github.com/mastodon/mastodon/pull/23701)) +- Change `ActivityPub::DeliveryWorker` retries to be spread out more ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21956)) + +### Fixed + +- Fix “Remove all followers from the selected domains” also removing follows and notifications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805)) +- Fix streaming metrics format ([emilweth](https://github.com/mastodon/mastodon/pull/23519), [emilweth](https://github.com/mastodon/mastodon/pull/23520)) +- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526)) +- Fix focus point of already-attached media not saving after edit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23566)) +- Fix sidebar behavior in settings/admin UI on mobile ([wxt2005](https://github.com/mastodon/mastodon/pull/23764)) +- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801)) +- Fix duplicate “Publish” button on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23804)) +- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787)) +- Fix server error when attempting to display the edit history of a trendable post in the admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23574)) +- Fix `tootctl accounts migrate` crashing because of a typo ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23567)) +- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957)) +- Fix the “Back” button in column headers sometimes leaving Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953)) +- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958)) +- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803)) +- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988)) +- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029)) +- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046)) +- Fix tags being unnecessarily stripped from plain-text short site description ([c960657](https://github.com/mastodon/mastodon/pull/23975)) +- Fix HTML entities not being un-escaped in extracted plain-text from remote posts ([c960657](https://github.com/mastodon/mastodon/pull/24019)) +- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751)) +- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611)) +- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568)) +- Fix duplicate mails being sent when the SMTP server is too slow to close the connection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23750)) + +### Security + +- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136)) +- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137)) + +## [4.1.0] - 2023-02-10 + +### Added + +- **Add support for importing/exporting server-wide domain blocks** ([enbylenore](https://github.com/mastodon/mastodon/pull/20597), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21471), [dariusk](https://github.com/mastodon/mastodon/pull/22803), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21470)) +- **Add listing of followed hashtags** ([connorshea](https://github.com/mastodon/mastodon/pull/21773)) +- **Add support for editing media description and focus point of already-sent posts** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20878)) + - Previously, you could add and remove attachments, but not edit media description of already-attached media + - REST API changes: + - `PUT /api/v1/statuses/:id` now takes an extra `media_attributes[]` array parameter with the `id` of the updated media and their updated `description`, `focus`, and `thumbnail` +- **Add follow request banner on account header** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20785)) + - REST API changes: + - `Relationship` entities have an extra `requested_by` boolean attribute representing whether the represented user has requested to follow you +- **Add confirmation screen when handling reports** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22375), [Gargron](https://github.com/mastodon/mastodon/pull/23156), [tribela](https://github.com/mastodon/mastodon/pull/23178)) +- Add option to make the landing page be `/about` even when trends are enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20808)) +- Add `noindex` setting back to the admin interface ([prplecake](https://github.com/mastodon/mastodon/pull/22205)) +- Add instance peers API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22810)) +- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833)) +- Add setting for status page URL ([Gargron](https://github.com/mastodon/mastodon/pull/23390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23499)) + - REST API changes: + - Add `configuration.urls.status` attribute to the object returned by `GET /api/v1/instance` +- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938)) +- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131)) +- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895)) +- Add `--remove-headers`, `--prune-profiles` and `--include-follows` flags to `tootctl media remove` ([evanphilip](https://github.com/mastodon/mastodon/pull/22149)) +- Add `--email` and `--dry-run` options to `tootctl accounts delete` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22328)) +- Add `tootctl accounts migrate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22330)) +- Add `tootctl accounts prune` ([tribela](https://github.com/mastodon/mastodon/pull/18397)) +- Add `tootctl domains purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22063)) +- Add `SIDEKIQ_CONCURRENCY` environment variable ([muffinista](https://github.com/mastodon/mastodon/pull/19589)) +- Add `DB_POOL` environment variable support for streaming server ([Gargron](https://github.com/mastodon/mastodon/pull/23470)) +- Add `MIN_THREADS` environment variable to set minimum Puma threads ([jimeh](https://github.com/mastodon/mastodon/pull/21048)) +- Add explanation text to log-in page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20946)) +- Add user profile OpenGraph tag on post pages ([bramus](https://github.com/mastodon/mastodon/pull/21423)) +- Add maskable icon support for Android ([workeffortwaste](https://github.com/mastodon/mastodon/pull/20904)) +- Add Belarusian to supported languages ([Mixaill](https://github.com/mastodon/mastodon/pull/22022)) +- Add Western Frisian to supported languages ([ykzts](https://github.com/mastodon/mastodon/pull/18602)) +- Add Montenegrin to the language picker ([ayefries](https://github.com/mastodon/mastodon/pull/21013)) +- Add Southern Sami and Lule Sami to the language picker ([Jullan-M](https://github.com/mastodon/mastodon/pull/21262)) +- Add logging for Rails cache timeouts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21667)) +- Add color highlight for active hashtag “follow” button ([MFTabriz](https://github.com/mastodon/mastodon/pull/21629)) +- Add brotli compression to `assets:precompile` ([Izorkin](https://github.com/mastodon/mastodon/pull/19025)) +- Add “disabled” account filter to the `/admin/accounts` UI ([tribela](https://github.com/mastodon/mastodon/pull/21282)) +- Add transparency to modal background for accessibility ([edent](https://github.com/mastodon/mastodon/pull/18081)) +- Add `lang` attribute to image description textarea and poll option field ([c960657](https://github.com/mastodon/mastodon/pull/23293)) +- Add `spellcheck` attribute to Content Warning and poll option input fields ([c960657](https://github.com/mastodon/mastodon/pull/23395)) +- Add `title` attribute to video elements in media attachments ([bramus](https://github.com/mastodon/mastodon/pull/21420)) +- Add left and right margins to emojis ([dsblank](https://github.com/mastodon/mastodon/pull/20464)) +- Add `roles` attribute to `Account` entities in REST API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23255), [tribela](https://github.com/mastodon/mastodon/pull/23428)) +- Add `reading:autoplay:gifs` to `/api/v1/preferences` ([j-f1](https://github.com/mastodon/mastodon/pull/22706)) +- Add `hide_collections` parameter to `/api/v1/accounts/credentials` ([CarlSchwan](https://github.com/mastodon/mastodon/pull/22790)) +- Add `policy` attribute to web push subscription objects in REST API at `/api/v1/push/subscriptions` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23210)) +- Add metrics endpoint to streaming API ([Gargron](https://github.com/mastodon/mastodon/pull/23388), [Gargron](https://github.com/mastodon/mastodon/pull/23469)) +- Add more specific error messages to HTTP signature verification ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21617)) +- Add Storj DCS to cloud object storage options in the `mastodon:setup` rake task ([jtolio](https://github.com/mastodon/mastodon/pull/21929)) +- Add checkmark symbol in the checkbox for sensitive media ([sidp](https://github.com/mastodon/mastodon/pull/22795)) +- Add missing accessibility attributes to logout link in modals ([kytta](https://github.com/mastodon/mastodon/pull/22549)) +- Add missing accessibility attributes to “Hide image” button in `MediaGallery` ([hs4man21](https://github.com/mastodon/mastodon/pull/22513)) +- Add missing accessibility attributes to hide content warning field when disabled ([hs4man21](https://github.com/mastodon/mastodon/pull/22568)) +- Add `aria-hidden` to footer circle dividers to improve accessibility ([hs4man21](https://github.com/mastodon/mastodon/pull/22576)) +- Add `lang` attribute to compose form inputs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23240)) + +### Changed + +- **Ensure exact match is the first result in hashtag searches** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21315)) +- Change account search to return followed accounts first ([dariusk](https://github.com/mastodon/mastodon/pull/22956)) +- Change batch account suspension to create a strike ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20897)) +- Change default reply language to match the default language when replying to a translated post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22272)) +- Change misleading wording about waitlists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20850)) +- Increase width of the unread notification border ([connorshea](https://github.com/mastodon/mastodon/pull/21692)) +- Change new post notification button on profiles to make it more apparent when it is enabled ([tribela](https://github.com/mastodon/mastodon/pull/22541)) +- Change trending tags admin interface to always show batch action controls ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23013)) +- Change wording of some OAuth scope descriptions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22491)) +- Change wording of admin report handling actions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18388)) +- Change confirm prompts for relationships management ([tribela](https://github.com/mastodon/mastodon/pull/19411)) +- Change language surrounding disability in prompts for media descriptions ([hs4man21](https://github.com/mastodon/mastodon/pull/20923)) +- Change confusing wording in the sign in banner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22490)) +- Change `POST /settings/applications/:id` to regenerate token on scopes change ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23359)) +- Change account moderation notes to make links clickable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22553)) +- Change link previews for statuses to never use avatar as fallback ([Gargron](https://github.com/mastodon/mastodon/pull/23376)) +- Change email address input to be read-only for logged-in users when requesting a new confirmation e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23247)) +- Change notifications per page from 15 to 40 in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/23348)) +- Change number of stored items in home feed from 400 to 800 ([Gargron](https://github.com/mastodon/mastodon/pull/23349)) +- Change API rate limits from 300/5min per user to 1500/5min per user, 300/5min per app ([Gargron](https://github.com/mastodon/mastodon/pull/23347)) +- Save avatar or header correctly even if the other one fails ([tribela](https://github.com/mastodon/mastodon/pull/18465)) +- Change `referrer-policy` to `same-origin` application-wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23037)) +- Add 'private' to `Cache-Control`, match Rails expectations ([daxtens](https://github.com/mastodon/mastodon/pull/20608)) +- Make the button that expands the compose form differentiable from the button that publishes a post ([Tak](https://github.com/mastodon/mastodon/pull/20864)) +- Change automatic post deletion configuration to be accessible to moved users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20774)) +- Make tag following idempotent ([trwnh](https://github.com/mastodon/mastodon/pull/20860), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/21285)) +- Use buildx functions for faster builds ([inductor](https://github.com/mastodon/mastodon/pull/20692)) +- Split off Dockerfile components for faster builds ([moritzheiber](https://github.com/mastodon/mastodon/pull/20933), [ineffyble](https://github.com/mastodon/mastodon/pull/20948), [BtbN](https://github.com/mastodon/mastodon/pull/21028)) +- Change last occurrence of “silence” to “limit” in UI text ([cincodenada](https://github.com/mastodon/mastodon/pull/20637)) +- Change “hide toot” to “hide post” ([seanthegeek](https://github.com/mastodon/mastodon/pull/22385)) +- Don't allow URLs that contain non-normalized paths to be verified ([dgl](https://github.com/mastodon/mastodon/pull/20999)) +- Change the “Trending now” header to be a link to the Explore page ([connorshea](https://github.com/mastodon/mastodon/pull/21759)) +- Change PostgreSQL connection timeout from 2 minutes to 15 seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21790)) +- Make handle more easily selectable on profile page ([cadars](https://github.com/mastodon/mastodon/pull/21479)) +- Allow admins to refresh remotely-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22327)) +- Change dropdown menu to contain “Copy link to post” even for non-public posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21316)) +- Allow adding relays in secure mode and limited federation mode ([ineffyble](https://github.com/mastodon/mastodon/pull/22324)) +- Change timestamps to be displayed using the user's timezone throughout the moderation interface ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21878), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22555)) +- Change CSP directives on API to be tight and concise ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20960)) +- Change web UI to not autofocus the compose form ([raboof](https://github.com/mastodon/mastodon/pull/16517), [Akkiesoft](https://github.com/mastodon/mastodon/pull/23094)) +- Change idempotency key handling for posting when database access is slow ([lambda](https://github.com/mastodon/mastodon/pull/21840)) +- Change remote media files to be downloaded outside of transactions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21796)) +- Improve contrast of charts in “poll has ended” notifications ([j-f1](https://github.com/mastodon/mastodon/pull/22575)) +- Change OEmbed detection and validation to be somewhat more lenient ([ineffyble](https://github.com/mastodon/mastodon/pull/22533)) +- Widen ElasticSearch version detection to not display a warning for OpenSearch ([VyrCossont](https://github.com/mastodon/mastodon/pull/22422), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23064)) +- Change link verification to allow pages larger than 1MB as long as the link is in the first 1MB ([untitaker](https://github.com/mastodon/mastodon/pull/22879)) +- Update default Node.js version to Node.js 16 ([ineffyble](https://github.com/mastodon/mastodon/pull/22223), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22342)) + +### Removed + +- Officially remove support for Ruby 2.6 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21477)) +- Remove `object-fit` polyfill used for old versions of Microsoft Edge ([shuuji3](https://github.com/mastodon/mastodon/pull/22693)) +- Remove `intersection-observer` polyfill for old Safari support ([shuuji3](https://github.com/mastodon/mastodon/pull/23284)) +- Remove empty `title` tag from mailer layout ([nametoolong](https://github.com/mastodon/mastodon/pull/23078)) +- Remove post count and last posts from ActivityPub representation of hashtag collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23460)) + +### Fixed + +- **Fix changing domain block severity not undoing individual account effects** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22135)) +- Fix suspension worker crashing on S3-compatible setups without ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22487)) +- Fix possible race conditions when suspending/unsuspending accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22363)) +- Fix being stuck in edit mode when deleting the edited posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22126)) +- Fix attached media uploads not being cleared when replying to a post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23504)) +- Fix filters not being applied to some notification types ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23211)) +- Fix incorrect link in push notifications for some event types ([elizabeth-dev](https://github.com/mastodon/mastodon/pull/23286)) +- Fix some performance issues with `/admin/instances` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21907)) +- Fix some pre-4.0 admin audit logs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22091)) +- Fix moderation audit log items for warnings having incorrect links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23242)) +- Fix account activation being sometimes triggered before email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23245)) +- Fix missing OAuth scopes for admin APIs ([trwnh](https://github.com/mastodon/mastodon/pull/20918), [trwnh](https://github.com/mastodon/mastodon/pull/20979)) +- Fix voter count not being cleared when a poll is reset ([afontenot](https://github.com/mastodon/mastodon/pull/21700)) +- Fix attachments of edited posts not being fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21565)) +- Fix irreversible and whole_word parameters handling in `/api/v1/filters` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21988)) +- Fix 500 error when marking posts as sensitive while some of them are deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22134)) +- Fix expanded posts not always being scrolled into view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21797)) +- Fix not being able to scroll the remote interaction modal on small screens ([xendke](https://github.com/mastodon/mastodon/pull/21763)) +- Fix not being able to scroll in post history modal ([cadars](https://github.com/mastodon/mastodon/pull/23396)) +- Fix audio player volume control on Safari ([minacle](https://github.com/mastodon/mastodon/pull/23187)) +- Fix disappearing “Explore” tabs on Safari ([nyura](https://github.com/mastodon/mastodon/pull/20917), [ykzts](https://github.com/mastodon/mastodon/pull/20982)) +- Fix wrong padding in RTL layout ([Gargron](https://github.com/mastodon/mastodon/pull/23157)) +- Fix drag & drop upload area display in single-column mode ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23217)) +- Fix being unable to get a single EmailDomainBlock from the admin API ([trwnh](https://github.com/mastodon/mastodon/pull/20846)) +- Fix admin-set follow recommandations being case-sensitive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23500)) +- Fix unserialized `role` on account entities in admin API ([Gargron](https://github.com/mastodon/mastodon/pull/23290)) +- Fix pagination of followed tags ([trwnh](https://github.com/mastodon/mastodon/pull/20861)) +- Fix dropdown menu positions when scrolling ([sidp](https://github.com/mastodon/mastodon/pull/22916), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23062)) +- Fix email with empty domain name labels passing validation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23246)) +- Fix mysterious registration failure when “Require a reason to join” is set with open registrations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22127)) +- Fix attachment rendering of edited posts in OpenGraph ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22270)) +- Fix invalid/empty RSS feed link on account pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20772)) +- Fix error in `VerifyLinkService` when processing links with no href ([joshuap](https://github.com/mastodon/mastodon/pull/20741)) +- Fix error in `VerifyLinkService` when processing links with invalid URLs ([untitaker](https://github.com/mastodon/mastodon/pull/23204)) +- Fix media uploads with FFmpeg 5 ([dead10ck](https://github.com/mastodon/mastodon/pull/21191)) +- Fix sensitive flag not being set when replying to a post with a content warning under certain conditions ([kedamaDQ](https://github.com/mastodon/mastodon/pull/21724)) +- Fix misleading message briefly showing up when loading follow requests under some conditions ([c960657](https://github.com/mastodon/mastodon/pull/23386)) +- Fix “Share @:user's profile” profile menu item not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21490)) +- Fix crash and incorrect behavior in `tootctl domains crawl` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19004)) +- Fix autoplay on iOS ([jamesadney](https://github.com/mastodon/mastodon/pull/21422)) +- Fix user clean-up scheduler crash when an unconfirmed account has a moderation note ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23318)) +- Fix spaces not being stripped in admin account search ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21324)) +- Fix spaces not being stripped when adding relays ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22655)) +- Fix infinite loading spinner instead of soft 404 for non-existing remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21303)) +- Fix minor visual issue with the top border of verified account fields ([j-f1](https://github.com/mastodon/mastodon/pull/22006)) +- Fix pending account approval and rejection not being recorded in the admin audit log ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/22088)) +- Fix “Sign up” button with closed registrations not opening modal on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22060)) +- Fix UI header overflowing on mobile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21783)) +- Fix 500 error when trying to migrate to an invalid address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21462)) +- Fix crash when trying to fetch unobtainable avatar of user using external authentication ([lochiiconnectivity](https://github.com/mastodon/mastodon/pull/22462)) +- Fix processing error on incoming malformed JSON-LD under some situations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23416)) +- Fix potential duplicate posts in Explore tab ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22121)) +- Fix deprecation warning in `tootctl accounts rotate` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22120)) +- Fix styling of featured tags in light theme ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23252)) +- Fix missing style in warning and strike cards ([AtelierSnek](https://github.com/mastodon/mastodon/pull/22177), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/22302)) +- Fix wasteful request to `/api/v1/custom_emojis` when not logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22326)) +- Fix replies sometimes being delivered to user-blocked domains ([tribela](https://github.com/mastodon/mastodon/pull/22117)) +- Fix admin dashboard crash when using some ElasticSearch replacements ([cortices](https://github.com/mastodon/mastodon/pull/21006)) +- Fix profile avatar being slightly offset into left border ([RiedleroD](https://github.com/mastodon/mastodon/pull/20994)) +- Fix N+1 queries in `NotificationsController` ([nametoolong](https://github.com/mastodon/mastodon/pull/21202)) +- Fix being unable to react to announcements with the keycap number sign emoji ([kescherCode](https://github.com/mastodon/mastodon/pull/22231)) +- Fix height computation of post embeds ([hodgesmr](https://github.com/mastodon/mastodon/pull/22141)) +- Fix accessibility issue of the search bar due to hidden placeholder ([alexstine](https://github.com/mastodon/mastodon/pull/21275)) +- Fix layout change handler not being removed due to a typo ([nschonni](https://github.com/mastodon/mastodon/pull/21829)) +- Fix typo in the default `S3_HOSTNAME` used in the `mastodon:setup` rake task ([danp](https://github.com/mastodon/mastodon/pull/19932)) +- Fix the top action bar appearing in the multi-column layout ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20943)) +- Fix inability to use local LibreTranslate without setting `ALLOWED_PRIVATE_ADDRESSES` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21926)) +- Fix punycoded local domains not being prettified in initial state ([Tritlo](https://github.com/mastodon/mastodon/pull/21440)) +- Fix CSP violation warning by removing inline CSS from SVG logo ([luxiaba](https://github.com/mastodon/mastodon/pull/20814)) +- Fix margin for search field on medium window size ([minacle](https://github.com/mastodon/mastodon/pull/21606)) +- Fix search popout scrolling with the page in single-column mode ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/16463)) +- Fix minor post cache hydration discrepancy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19879)) +- Fix `・` detection in hashtags ([parthoghosh24](https://github.com/mastodon/mastodon/pull/22888)) +- Fix hashtag follows bypassing user blocks ([tribela](https://github.com/mastodon/mastodon/pull/22849)) +- Fix moved accounts being incorrectly redirected to account settings when trying to view a remote profile ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22497)) +- Fix site upload validations ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22479)) +- Fix “Add new domain block” button using last submitted search value instead of the current one ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22485)) +- Fix misleading hashtag warning when posting with “Followers only” or “Mentioned people only” visibility ([n0toose](https://github.com/mastodon/mastodon/pull/22827)) +- Fix embedded posts with videos grabbing focus ([Akkiesoft](https://github.com/mastodon/mastodon/pull/22778)) +- Fix `$` not being escaped in `.env.production` files generated by the `mastodon:setup` rake task ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23012), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23072)) +- Fix sanitizer parsing link text as HTML when stripping unsupported links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22558)) +- Fix `scheduled_at` input not using `datetime-local` when editing announcements ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21896)) +- Fix REST API serializer for `Account` not including `moved` when the moved account has itself moved ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22483)) +- Fix `/api/v1/admin/trends/tags` using wrong serializer ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18943)) +- Fix situations in which instance actor can be set to a Mastodon-incompatible name ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22307)) + +### Security + +- Add `form-action` CSP directive ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20781), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20958), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/20962)) +- Fix unbounded recursion in account discovery ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22025)) +- Revoke all authorized applications on password reset ([FrancisMurillo](https://github.com/mastodon/mastodon/pull/21325)) +- Fix unbounded recursion in post discovery ([ClearlyClaire,nametoolong](https://github.com/mastodon/mastodon/pull/23506)) + ## [4.0.2] - 2022-11-15 + ### Fixed - Fix wrong color on mentions hidden behind content warning in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/20724)) @@ -11,6 +291,7 @@ All notable changes to this project will be documented in this file. - Fix `unsafe-eval` being used when `wasm-unsafe-eval` is enough in Content Security Policy ([Gargron](https://github.com/mastodon/mastodon/pull/20729), [prplecake](https://github.com/mastodon/mastodon/pull/20606)) ## [4.0.1] - 2022-11-14 + ### Fixed - Fix nodes order being sometimes mangled when rewriting emoji ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20677)) @@ -214,6 +495,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix out-of-bound reads in blurhash transcoder ([delroth](https://github.com/mastodon/mastodon/pull/20388)) ## [3.5.3] - 2022-05-26 + ### Added - **Add language dropdown to compose form in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/18420), [ykzts](https://github.com/mastodon/mastodon/pull/18460)) @@ -261,6 +543,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix confirmation redirect to app without `Location` header ([Gargron](https://github.com/mastodon/mastodon/pull/18523)) ## [3.5.2] - 2022-05-04 + ### Added - Add warning on direct messages screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/18289)) @@ -313,6 +596,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix error in alias settings page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18004)) ## [3.5.1] - 2022-04-08 + ### Added - Add pagination for trending statuses in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17976)) @@ -356,6 +640,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix error when indexing statuses into Elasticsearch ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17912)) ## [3.5.0] - 2022-03-30 + ### Added - **Add support for incoming edited posts** ([Gargron](https://github.com/mastodon/mastodon/pull/16697), [Gargron](https://github.com/mastodon/mastodon/pull/17727), [Gargron](https://github.com/mastodon/mastodon/pull/17728), [Gargron](https://github.com/mastodon/mastodon/pull/17320), [Gargron](https://github.com/mastodon/mastodon/pull/17404), [Gargron](https://github.com/mastodon/mastodon/pull/17390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17335), [Gargron](https://github.com/mastodon/mastodon/pull/17696), [Gargron](https://github.com/mastodon/mastodon/pull/17745), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17740), [Gargron](https://github.com/mastodon/mastodon/pull/17697), [Gargron](https://github.com/mastodon/mastodon/pull/17648), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17531), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17499), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17498), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17380), [Gargron](https://github.com/mastodon/mastodon/pull/17373), [Gargron](https://github.com/mastodon/mastodon/pull/17334), [Gargron](https://github.com/mastodon/mastodon/pull/17333), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17699), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17748)) @@ -555,6 +840,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix being able to bypass e-mail restrictions ([Gargron](https://github.com/mastodon/mastodon/pull/17909)) ## [3.4.6] - 2022-02-03 + ### Fixed - Fix `mastodon:webpush:generate_vapid_key` task requiring a functional environment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17338)) @@ -569,6 +855,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Disable legacy XSS filtering ([Wonderfall](https://github.com/mastodon/mastodon/pull/17289)) ## [3.4.5] - 2022-01-31 + ### Added - Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393)) @@ -583,6 +870,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix followers synchronization mechanism ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16510)) ## [3.4.4] - 2021-11-26 + ### Fixed - Fix error when suspending user with an already blocked canonical email ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17036)) @@ -600,11 +888,13 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix handling of recursive toots in WebUI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17041)) ## [3.4.3] - 2021-11-06 + ### Fixed - Fix login being broken due to inaccurately applied backport fix in 3.4.2 ([Gargron](https://github.com/mastodon/mastodon/commit/5c47a18c8df3231aa25c6d1f140a71a7fac9cbf9)) ## [3.4.2] - 2021-11-06 + ### Added - Add `configuration` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/16485)) @@ -648,6 +938,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix revoking a specific session not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943)) ## [3.4.1] - 2021-06-03 + ### Added - Add new emoji assets from Twemoji 13.1.0 ([Gargron](https://github.com/mastodon/mastodon/pull/16345)) @@ -667,6 +958,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix mailer jobs for deleted notifications erroring out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16294)) ## [3.4.0] - 2021-05-16 + ### Added - **Add follow recommendations for onboarding** ([Gargron](https://github.com/mastodon/mastodon/pull/15945), [Gargron](https://github.com/mastodon/mastodon/pull/16161), [Gargron](https://github.com/mastodon/mastodon/pull/16060), [Gargron](https://github.com/mastodon/mastodon/pull/16077), [Gargron](https://github.com/mastodon/mastodon/pull/16078), [Gargron](https://github.com/mastodon/mastodon/pull/16160), [Gargron](https://github.com/mastodon/mastodon/pull/16079), [noellabo](https://github.com/mastodon/mastodon/pull/16044), [noellabo](https://github.com/mastodon/mastodon/pull/16045), [Gargron](https://github.com/mastodon/mastodon/pull/16152), [Gargron](https://github.com/mastodon/mastodon/pull/16153), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16082), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16173), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16159), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16189)) @@ -702,7 +994,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - This method allows an app through which a user signed-up to request a new confirmation e-mail to be sent, or to change the e-mail of the account before it is confirmed - Add `GET /api/v1/accounts/lookup` to REST API ([Gargron](https://github.com/mastodon/mastodon/pull/15740), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15750)) - This method allows to quickly convert a username of a known account to an ID that can be used with the REST API, or to check if a username is available - for sign-up + for sign-up - Add `policy` param to `POST /api/v1/push/subscriptions` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/16040)) - This param allows an app to control from whom notifications should be delivered as push notifications to the app - Add `details` to error response for `POST /api/v1/accounts` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/15803)) @@ -812,6 +1104,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix app name, website and redirect URIs not having a maximum length ([Gargron](https://github.com/mastodon/mastodon/pull/16042)) ## [3.3.0] - 2020-12-27 + ### Added - **Add hotkeys for audio/video control in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/15158), [Gargron](https://github.com/mastodon/mastodon/pull/15198)) @@ -988,6 +1281,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix resolving accounts sometimes creating duplicate records for a given ActivityPub identifier ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15364)) ## [3.2.2] - 2020-12-19 + ### Added - Add `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14860), [Gargron](https://github.com/mastodon/mastodon/pull/15223)) @@ -1014,6 +1308,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix resolving accounts sometimes creating duplicate records for a given ActivityPub identifier ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15364)) ## [3.2.1] - 2020-10-19 + ### Added - Add support for latest HTTP Signatures spec draft ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14556)) @@ -1043,6 +1338,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix files served as `application/octet-stream` being rejected without attempting mime type detection ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14452)) ## [3.2.0] - 2020-07-27 + ### Added - Add `SMTP_SSL` environment variable ([OmmyZhang](https://github.com/mastodon/mastodon/pull/14309)) @@ -1178,7 +1474,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix unique username constraint for local users not being enforced in database ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14099)) - Fix unnecessary gap under video modal in web UI ([mfmfuyu](https://github.com/mastodon/mastodon/pull/14098)) - Fix 2FA and sign in token pages not respecting user locale ([mfmfuyu](https://github.com/mastodon/mastodon/pull/14087)) -- Fix unapproved users being able to view profiles when in limited-federation mode *and* requiring approval for sign-ups ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14093)) +- Fix unapproved users being able to view profiles when in limited-federation mode _and_ requiring approval for sign-ups ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14093)) - Fix initial audio volume not corresponding to what's displayed in audio player in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14057)) - Fix timelines sometimes jumping when closing modals in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14019)) - Fix memory usage of downloading remote files ([Gargron](https://github.com/mastodon/mastodon/pull/14184), [Gargron](https://github.com/mastodon/mastodon/pull/14181), [noellabo](https://github.com/mastodon/mastodon/pull/14356)) @@ -1196,6 +1492,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Clear out media attachments in a separate worker (slow) ## [3.1.5] - 2020-07-07 + ### Security - Fix media attachment enumeration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14254)) @@ -1203,6 +1500,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix other sessions not being logged out on password change ([Gargron](https://github.com/mastodon/mastodon/pull/14252)) ## [3.1.4] - 2020-05-14 + ### Added - Add `vi` to available locales ([taicv](https://github.com/mastodon/mastodon/pull/13542)) @@ -1241,7 +1539,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix regression in `tootctl media remove-orphans` ([Gargron](https://github.com/mastodon/mastodon/pull/13405)) - Fix old unique jobs digests not having been cleaned up ([Gargron](https://github.com/mastodon/mastodon/pull/13683)) - Fix own following/followers not showing muted users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13614)) -- Fix list of followed people ignoring sorting on Follows & Followers page ([taras2358](https://github.com/mastodon/mastodon/pull/13676)) +- Fix list of followed people ignoring sorting on Follows & Followers page ([taras2358](https://github.com/mastodon/mastodon/pull/13676)) - Fix wrong pgHero Content-Security-Policy when `CDN_HOST` is set ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13595)) - Fix needlessly deduplicating usernames on collisions with remote accounts when signing-up through SAML/CAS ([kaiyou](https://github.com/mastodon/mastodon/pull/13581)) - Fix page incorrectly scrolling when bringing up dropdown menus in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13574)) @@ -1270,6 +1568,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - The issue only affects developers of apps who are shared between multiple users, such as server-side apps like cross-posters ## [3.1.3] - 2020-04-05 + ### Added - Add ability to filter audit log in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/13381)) @@ -1343,6 +1642,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix re-sending of e-mail confirmation not being rate limited ([Gargron](https://github.com/mastodon/mastodon/pull/13360)) ## [v3.1.2] - 2020-02-27 + ### Added - Add `--reset-password` option to `tootctl accounts modify` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13126)) @@ -1369,11 +1669,13 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix leak of arbitrary statuses through unfavourite action in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/13161)) ## [3.1.1] - 2020-02-10 + ### Fixed - Fix yanked dependency preventing installation ([mayaeh](https://github.com/mastodon/mastodon/pull/13059)) ## [3.1.0] - 2020-02-09 + ### Added - Add bookmarks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/7107), [Gargron](https://github.com/mastodon/mastodon/pull/12494), [Gomasy](https://github.com/mastodon/mastodon/pull/12381)) @@ -1538,6 +1840,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix settings pages being cacheable by the browser ([Gargron](https://github.com/mastodon/mastodon/pull/12714)) ## [3.0.1] - 2019-10-10 + ### Added - Add `tootctl media usage` command ([Gargron](https://github.com/mastodon/mastodon/pull/12115)) @@ -1571,6 +1874,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix `tootctl accounts cull` advertising unused option flag ([Kjwon15](https://github.com/mastodon/mastodon/pull/12074)) ## [3.0.0] - 2019-10-03 + ### Added - Add "not available" label to unloaded media attachments in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11715), [Gargron](https://github.com/mastodon/mastodon/pull/11745)) @@ -1767,6 +2071,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix performance of GIF re-encoding and always strip EXIF data from videos ([Gargron](https://github.com/mastodon/mastodon/pull/12057)) ## [2.9.3] - 2019-08-10 + ### Added - Add GIF and WebP support for custom emojis ([Gargron](https://github.com/mastodon/mastodon/pull/11519)) @@ -1826,6 +2131,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix blocked domains still being able to fill database with account records ([Gargron](https://github.com/mastodon/mastodon/pull/11219)) ## [2.9.2] - 2019-06-22 + ### Added - Add `short_description` and `approval_required` to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/11146)) @@ -1840,6 +2146,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix audio not being downloaded from remote servers ([Gargron](https://github.com/mastodon/mastodon/pull/11145)) ## [2.9.1] - 2019-06-22 + ### Added - Add moderation API ([Gargron](https://github.com/mastodon/mastodon/pull/9387)) @@ -1865,6 +2172,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix scrolling behaviour in compose form ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11093)) ## [2.9.0] - 2019-06-13 + ### Added - **Add single-column mode in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/10807), [Gargron](https://github.com/mastodon/mastodon/pull/10848), [Gargron](https://github.com/mastodon/mastodon/pull/11003), [Gargron](https://github.com/mastodon/mastodon/pull/10961), [Hanage999](https://github.com/mastodon/mastodon/pull/10915), [noellabo](https://github.com/mastodon/mastodon/pull/10917), [abcang](https://github.com/mastodon/mastodon/pull/10859), [Gargron](https://github.com/mastodon/mastodon/pull/10820), [Gargron](https://github.com/mastodon/mastodon/pull/10835), [Gargron](https://github.com/mastodon/mastodon/pull/10809), [Gargron](https://github.com/mastodon/mastodon/pull/10963), [noellabo](https://github.com/mastodon/mastodon/pull/10883), [Hanage999](https://github.com/mastodon/mastodon/pull/10839)) @@ -1919,6 +2227,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix login sometimes redirecting to paths that are not pages ([Gargron](https://github.com/mastodon/mastodon/pull/11019)) ## [2.8.4] - 2019-05-24 + ### Fixed - Fix delivery not retrying on some inbox errors that should be retriable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10812)) @@ -1930,6 +2239,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Require specific OAuth scopes for specific endpoints of the streaming API, instead of merely requiring a token for all endpoints, and allow using WebSockets protocol negotiation to specify the access token instead of using a query string ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10818)) ## [2.8.3] - 2019-05-19 + ### Added - Add `og:image:alt` OpenGraph tag ([BenLubar](https://github.com/mastodon/mastodon/pull/10779)) @@ -1952,6 +2262,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix "invited by" not showing up in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10791)) ## [2.8.2] - 2019-05-05 + ### Added - Add `SOURCE_TAG` environment variable ([ushitora-anqou](https://github.com/mastodon/mastodon/pull/10698)) @@ -1964,6 +2275,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix closing video modal scrolling timelines to top ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10695)) ## [2.8.1] - 2019-05-04 + ### Added - Add link to existing domain block when trying to block an already-blocked domain ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10663)) @@ -2003,6 +2315,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix confirmation modals being too narrow for a secondary action button ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10586)) ## [2.8.0] - 2019-04-10 + ### Added - Add polls ([Gargron](https://github.com/mastodon/mastodon/pull/10111), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10155), [Gargron](https://github.com/mastodon/mastodon/pull/10184), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10196), [Gargron](https://github.com/mastodon/mastodon/pull/10248), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10255), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10322), [Gargron](https://github.com/mastodon/mastodon/pull/10138), [Gargron](https://github.com/mastodon/mastodon/pull/10139), [Gargron](https://github.com/mastodon/mastodon/pull/10144), [Gargron](https://github.com/mastodon/mastodon/pull/10145),[Gargron](https://github.com/mastodon/mastodon/pull/10146), [Gargron](https://github.com/mastodon/mastodon/pull/10148), [Gargron](https://github.com/mastodon/mastodon/pull/10151), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10150), [Gargron](https://github.com/mastodon/mastodon/pull/10168), [Gargron](https://github.com/mastodon/mastodon/pull/10165), [Gargron](https://github.com/mastodon/mastodon/pull/10172), [Gargron](https://github.com/mastodon/mastodon/pull/10170), [Gargron](https://github.com/mastodon/mastodon/pull/10171), [Gargron](https://github.com/mastodon/mastodon/pull/10186), [Gargron](https://github.com/mastodon/mastodon/pull/10189), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10200), [rinsuki](https://github.com/mastodon/mastodon/pull/10203), [Gargron](https://github.com/mastodon/mastodon/pull/10213), [Gargron](https://github.com/mastodon/mastodon/pull/10246), [Gargron](https://github.com/mastodon/mastodon/pull/10265), [Gargron](https://github.com/mastodon/mastodon/pull/10261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10333), [Gargron](https://github.com/mastodon/mastodon/pull/10352), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10140), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10142), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10141), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10161), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10158), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10156), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10160), [Gargron](https://github.com/mastodon/mastodon/pull/10185), [Gargron](https://github.com/mastodon/mastodon/pull/10188), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10195), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10208), [Gargron](https://github.com/mastodon/mastodon/pull/10187), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10214), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/10209)) @@ -2086,6 +2399,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix `tootctl accounts cull` sometimes removing accounts that are temporarily unreachable ([BenLubar](https://github.com/mastodon/mastodon/pull/10460)) ## [2.7.4] - 2019-03-05 + ### Fixed - Fix web UI not cleaning up notifications after block ([Gargron](https://github.com/mastodon/mastodon/pull/10108)) @@ -2100,6 +2414,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix edit profile page crash for suspended-then-unsuspended users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10178)) ## [2.7.3] - 2019-02-23 + ### Added - Add domain filter to the admin federation page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10071)) @@ -2117,6 +2432,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Change custom emojis to randomize stored file name ([hinaloe](https://github.com/mastodon/mastodon/pull/10090)) ## [2.7.2] - 2019-02-17 + ### Added - Add support for IPv6 in e-mail validation ([zoc](https://github.com/mastodon/mastodon/pull/10009)) @@ -2158,6 +2474,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Change error graphic to hover-to-play ([Gargron](https://github.com/mastodon/mastodon/pull/10055)) ## [2.7.1] - 2019-01-28 + ### Fixed - Fix SSO authentication not working due to missing agreement boolean ([Gargron](https://github.com/mastodon/mastodon/pull/9915)) @@ -2172,6 +2489,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix missing strong style for landing page description ([Kjwon15](https://github.com/mastodon/mastodon/pull/9892)) ## [2.7.0] - 2019-01-20 + ### Added - Add link for adding a user to a list from their profile ([namelessGonbai](https://github.com/mastodon/mastodon/pull/9062)) @@ -2301,6 +2619,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Add tombstones for remote statuses to prevent replay attacks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9830)) ## [2.6.5] - 2018-12-01 + ### Changed - Change lists to display replies to others on the list and list owner ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9324)) @@ -2310,11 +2629,13 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix failures caused by commonly-used JSON-LD contexts being unavailable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9412)) ## [2.6.4] - 2018-11-30 + ### Fixed - Fix yarn dependencies not installing due to yanked event-stream package ([Gargron](https://github.com/mastodon/mastodon/pull/9401)) ## [2.6.3] - 2018-11-30 + ### Added - Add hyphen to characters allowed in remote usernames ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9345)) @@ -2334,6 +2655,7 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix TLS handshake timeout not being enforced ([Gargron](https://github.com/mastodon/mastodon/pull/9381)) ## [2.6.2] - 2018-11-23 + ### Added - Add Page to whitelisted ActivityPub types ([mbajur](https://github.com/mastodon/mastodon/pull/9188)) @@ -2368,12 +2690,14 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix HTTP connection timeout of 10s not being enforced ([Gargron](https://github.com/mastodon/mastodon/pull/9329)) ## [2.6.1] - 2018-10-30 + ### Fixed - Fix resolving resources by URL not working due to a regression in [valerauko](https://github.com/mastodon/mastodon/pull/9132) ([Gargron](https://github.com/mastodon/mastodon/pull/9171)) - Fix reducer error in web UI when a conversation has no last status ([Gargron](https://github.com/mastodon/mastodon/pull/9173)) ## [2.6.0] - 2018-10-30 + ### Added - Add link ownership verification ([Gargron](https://github.com/mastodon/mastodon/pull/8703)) @@ -2478,11 +2802,13 @@ Some of the features in this release have been funded through the [NGI0 Discover - Fix handling of content types with profile ([valerauko](https://github.com/mastodon/mastodon/pull/9132)) ## [2.5.2] - 2018-10-12 + ### Security - Fix XSS vulnerability ([Gargron](https://github.com/mastodon/mastodon/pull/8959)) ## [2.5.1] - 2018-10-07 + ### Fixed - Fix database migrations for PostgreSQL below 9.5 ([Gargron](https://github.com/mastodon/mastodon/pull/8903)) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b3eafdc482..2ee2e538bc 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ed670f5a6a..a232915b6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -# Contributing to Mastodon Glitch Edition # +# Contributing to Mastodon Glitch Edition Thank you for your interest in contributing to the `glitch-soc` project! Here are some guidelines, and ways you can help. -> (This document is a bit of a work-in-progress, so please bear with us. -> If you don't see what you're looking for here, please don't hesitate to reach out!) +> (This document is a bit of a work-in-progress, so please bear with us. +> If you don't see what you're looking for here, please don't hesitate to reach out!) ## Translations @@ -12,34 +12,32 @@ You can submit glitch-soc-specific translations via [Crowdin](https://crowdin.co [![Crowdin](https://badges.crowdin.net/glitch-soc/localized.svg)](https://crowdin.com/project/glitch-soc) -## Planning ## +## Planning Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects. We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler. -## Documentation ## +## Documentation The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)). Right now, we've mostly focused on the features that make this fork different from upstream in some manner. Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code. -## Frontend Development ## +## Frontend Development Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information. -## Backend Development ## +## Backend Development See the guidelines below. - - - - +--- You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `mastodon/mastodon`, reproduced below.
-CONTRIBUTING -======= -Contributing +# Contributing Thank you for considering contributing to Mastodon 🐘 @@ -68,9 +66,9 @@ You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). Example: -|Not ideal|Better| -|---|----| -|Fixed NoMethodError in RemovalWorker|Fix nil error when removing statuses caused by race condition| +| Not ideal | Better | +| ------------------------------------ | ------------------------------------------------------------- | +| Fixed NoMethodError in RemovalWorker | Fix nil error when removing statuses caused by race condition | It is not always possible to phrase every change in such a manner, but it is desired. @@ -82,8 +80,6 @@ It is not always possible to phrase every change in such a manner, but it is des - Code style rules (rubocop, eslint) - Normalization of locale files (i18n-tasks) -**Note**: You may need to log in and authorise the GitHub account your fork of this repository belongs to with CircleCI to enable some of the automated checks to run. - ## Documentation The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation). diff --git a/Capfile b/Capfile index bf3ae7e249..86efa5bacf 100644 --- a/Capfile +++ b/Capfile @@ -1,4 +1,5 @@ # frozen_string_literal: true + require 'capistrano/setup' require 'capistrano/deploy' require 'capistrano/scm/git' diff --git a/Dockerfile b/Dockerfile index ce7f4d7186..cb5096581c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # syntax=docker/dockerfile:1.4 # This needs to be bullseye-slim because the Ruby image is built on bullseye-slim -ARG NODE_VERSION="16.18.1-bullseye-slim" +ARG NODE_VERSION="16.20-bullseye-slim" -FROM ghcr.io/moritzheiber/ruby-jemalloc:3.0.4-slim as ruby +FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby FROM node:${NODE_VERSION} as build COPY --link --from=ruby /opt/ruby /opt/ruby @@ -18,7 +18,6 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/ # hadolint ignore=DL3008 RUN apt-get update && \ apt-get install -y --no-install-recommends build-essential \ - ca-certificates \ git \ libicu-dev \ libidn11-dev \ @@ -37,10 +36,15 @@ RUN apt-get update && \ bundle config set --local without 'development test' && \ bundle config set silence_root_warning true && \ bundle install -j"$(nproc)" && \ - yarn install --pure-lockfile --network-timeout 600000 + yarn install --pure-lockfile --production --network-timeout 600000 && \ + yarn cache clean FROM node:${NODE_VERSION} +# Use those args to specify your own version flags & suffixes +ARG MASTODON_VERSION_FLAGS="" +ARG MASTODON_VERSION_SUFFIX="" + ARG UID="991" ARG GID="991" @@ -51,7 +55,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" -# Ignoreing these here since we don't want to pin any versions and the Debian image removes apt-get content after use +# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use # hadolint ignore=DL3008,DL3009 RUN apt-get update && \ echo "Etc/UTC" > /etc/localtime && \ @@ -84,15 +88,16 @@ COPY --chown=mastodon:mastodon --from=build /opt/mastodon /opt/mastodon ENV RAILS_ENV="production" \ NODE_ENV="production" \ RAILS_SERVE_STATIC_FILES="true" \ - BIND="0.0.0.0" + BIND="0.0.0.0" \ + MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \ + MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}" # Set the run user USER mastodon WORKDIR /opt/mastodon # Precompile assets -RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile && \ - yarn cache clean +RUN OTP_SECRET=precompile_placeholder SECRET_KEY_BASE=precompile_placeholder rails assets:precompile # Set the work dir and the container entry point ENTRYPOINT ["/usr/bin/tini", "--"] diff --git a/Gemfile b/Gemfile index 1af346093a..7a0fbdc82d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,46 +1,43 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 2.7.0', '< 3.1.0' +ruby '>= 3.0.0' -gem 'pkg-config', '~> 1.5' -gem 'rexml', '~> 3.2' - -gem 'puma', '~> 5.6' +gem 'puma', '~> 6.3' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' -gem 'rack', '~> 2.2.6' +gem 'rack', '~> 2.2.7' -gem 'hamlit-rails', '~> 0.2' -gem 'pg', '~> 1.4' +gem 'haml-rails', '~>2.0' +gem 'pg', '~> 1.5' gem 'makara', '~> 0.5' -gem 'pghero', '~> 2.8' +gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.117', require: false +gem 'aws-sdk-s3', '~> 1.123', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.1' +gem 'kt-paperclip', '~> 7.2' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' -gem 'bootsnap', '~> 1.15.0', require: false +gem 'bootsnap', '~> 1.16.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' -gem 'chewy', '~> 7.2' -gem 'devise', '~> 4.8' -gem 'devise-two-factor', '~> 4.0' +gem 'chewy', '~> 7.3' +gem 'devise', '~> 4.9' +gem 'devise-two-factor', '~> 4.1' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' end -gem 'net-ldap', '~> 0.17' +gem 'net-ldap', '~> 0.18' gem 'omniauth-cas', '~> 2.0' gem 'omniauth-saml', '~> 1.10' -gem 'gitlab-omniauth-openid-connect', '~>0.10.0', require: 'omniauth_openid_connect' +gem 'omniauth_openid_connect', '~> 0.6.1' gem 'omniauth', '~> 1.9' gem 'omniauth-rails_csrf_protection', '~> 0.1' @@ -60,9 +57,8 @@ gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' -gem 'nokogiri', '~> 1.14' -gem 'nsa', '~> 0.2' -gem 'oj', '~> 3.13' +gem 'nokogiri', '~> 1.15' +gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' gem 'posix-spawn' @@ -70,84 +66,117 @@ gem 'public_suffix', '~> 5.0' gem 'pundit', '~> 2.3' gem 'premailer-rails' gem 'rack-attack', '~> 6.6' -gem 'rack-cors', '~> 1.1', require: 'rack/cors' +gem 'rack-cors', '~> 2.0', require: 'rack/cors' gem 'rails-i18n', '~> 6.0' -gem 'rails-settings-cached', '~> 0.6' -gem 'redcarpet', '~> 3.5' +gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' +gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'rqrcode', '~> 2.1' -gem 'ruby-progressbar', '~> 1.11' +gem 'rqrcode', '~> 2.2' +gem 'ruby-progressbar', '~> 1.13' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' -gem 'sidekiq-scheduler', '~> 4.0' +gem 'sidekiq-scheduler', '~> 5.0' gem 'sidekiq-unique-jobs', '~> 7.1' gem 'sidekiq-bulk', '~> 0.2.0' gem 'simple-navigation', '~> 4.4' -gem 'simple_form', '~> 5.1' +gem 'simple_form', '~> 5.2' gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' gem 'stoplight', '~> 3.0.1' -gem 'strong_migrations', '~> 0.7' +gem 'strong_migrations', '~> 0.8' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' -gem 'tzinfo-data', '~> 1.2022' +gem 'tzinfo-data', '~> 1.2023' gem 'webpacker', '~> 5.4' gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' -gem 'webauthn', '~> 2.5' +gem 'webauthn', '~> 3.0' gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -group :development, :test do - gem 'fabrication', '~> 2.30' - gem 'fuubar', '~> 2.5' - gem 'i18n-tasks', '~> 1.0', require: false - gem 'pry-byebug', '~> 3.10' - gem 'pry-rails', '~> 0.3' - gem 'rspec-rails', '~> 5.1' - gem 'rubocop-performance', require: false - gem 'rubocop-rails', require: false - gem 'rubocop-rspec', require: false - gem 'rubocop', require: false -end - -group :production, :test do - gem 'private_address_check', '~> 0.5' -end +gem 'private_address_check', '~> 0.5' group :test do - gem 'capybara', '~> 3.38' - gem 'climate_control', '~> 0.2' - gem 'faker', '~> 3.1' - gem 'json-schema', '~> 3.0' - gem 'rack-test', '~> 2.0' - gem 'rails-controller-testing', '~> 1.0' - gem 'rspec_junit_formatter', '~> 0.6' + # RSpec runner for rails + gem 'rspec-rails', '~> 6.0' + + # Used to split testing into chunks in CI + gem 'rspec_chunked', '~> 0.6' + + # RSpec progress bar formatter + gem 'fuubar', '~> 2.5' + + # Extra RSpec extenion methods and helpers for sidekiq gem 'rspec-sidekiq', '~> 3.1' + + # Browser integration testing + gem 'capybara', '~> 3.39' + + # Used to mock environment variables + gem 'climate_control', '~> 0.2' + + # Generating fake data for specs + gem 'faker', '~> 3.2' + + # Generate test objects for specs + gem 'fabrication', '~> 2.30' + + # Add back helpers functions removed in Rails 5.1 + gem 'rails-controller-testing', '~> 1.0' + + # Validate schemas in specs + gem 'json-schema', '~> 4.0' + + # Test harness fo rack components + gem 'rack-test', '~> 2.1' + + # Coverage formatter for RSpec test if DISABLE_SIMPLECOV is false gem 'simplecov', '~> 0.22', require: false + + # Stub web requests for specs gem 'webmock', '~> 3.18' end group :development do - gem 'active_record_query_trace', '~> 1.8' + # Code linting CLI and plugins + gem 'rubocop', require: false + gem 'rubocop-capybara', require: false + gem 'rubocop-performance', require: false + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', require: false + + # Annotates modules with schema gem 'annotate', '~> 3.2' + + # Enhanced error message pages for development gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 1.0' - gem 'bullet', '~> 7.0' + + # Preview mail in the browser gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' - gem 'memory_profiler' + + # Security analysis CLI tools gem 'brakeman', '~> 5.4', require: false gem 'bundler-audit', '~> 0.9', require: false + # Linter CLI for HAML files + gem 'haml_lint', require: false + + # Deployment automation gem 'capistrano', '~> 3.17' gem 'capistrano-rails', '~> 1.6' gem 'capistrano-rbenv', '~> 2.2' gem 'capistrano-yarn', '~> 2.0' - gem 'stackprof' + # Validate missing i18n keys + gem 'i18n-tasks', '~> 1.0', require: false + + # Profiling tools + gem 'memory_profiler', require: false + gem 'stackprof', require: false end group :production do @@ -158,5 +187,9 @@ gem 'concurrent-ruby', require: false gem 'connection_pool', require: false gem 'xorcist', '~> 1.1' -gem 'hcaptcha', '~> 7.1' gem 'cocoon', '~> 1.2' + +gem 'net-http', '~> 0.3.2' +gem 'rubyzip', '~> 2.3' + +gem 'hcaptcha', '~> 7.1' diff --git a/Gemfile.lock b/Gemfile.lock index accb3744cd..b2d75e9d4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,43 +7,51 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) +GIT + remote: https://github.com/mastodon/rails-settings-cached.git + revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab + branch: v0.6.6-aliases-true + specs: + rails-settings-cached (0.6.6) + rails (>= 4.2.0) + GEM remote: https://rubygems.org/ specs: - actioncable (6.1.7.1) - actionpack (= 6.1.7.1) - activesupport (= 6.1.7.1) + actioncable (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.7.1) - actionpack (= 6.1.7.1) - activejob (= 6.1.7.1) - activerecord (= 6.1.7.1) - activestorage (= 6.1.7.1) - activesupport (= 6.1.7.1) + actionmailbox (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (>= 2.7.1) - actionmailer (6.1.7.1) - actionpack (= 6.1.7.1) - actionview (= 6.1.7.1) - activejob (= 6.1.7.1) - activesupport (= 6.1.7.1) + actionmailer (6.1.7.4) + actionpack (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activesupport (= 6.1.7.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.7.1) - actionview (= 6.1.7.1) - activesupport (= 6.1.7.1) + actionpack (6.1.7.4) + actionview (= 6.1.7.4) + activesupport (= 6.1.7.4) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.7.1) - actionpack (= 6.1.7.1) - activerecord (= 6.1.7.1) - activestorage (= 6.1.7.1) - activesupport (= 6.1.7.1) + actiontext (6.1.7.4) + actionpack (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) nokogiri (>= 1.8.5) - actionview (6.1.7.1) - activesupport (= 6.1.7.1) + actionview (6.1.7.4) + activesupport (= 6.1.7.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -53,29 +61,28 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - active_record_query_trace (1.8) - activejob (6.1.7.1) - activesupport (= 6.1.7.1) + activejob (6.1.7.4) + activesupport (= 6.1.7.4) globalid (>= 0.3.6) - activemodel (6.1.7.1) - activesupport (= 6.1.7.1) - activerecord (6.1.7.1) - activemodel (= 6.1.7.1) - activesupport (= 6.1.7.1) - activestorage (6.1.7.1) - actionpack (= 6.1.7.1) - activejob (= 6.1.7.1) - activerecord (= 6.1.7.1) - activesupport (= 6.1.7.1) + activemodel (6.1.7.4) + activesupport (= 6.1.7.4) + activerecord (6.1.7.4) + activemodel (= 6.1.7.4) + activesupport (= 6.1.7.4) + activestorage (6.1.7.4) + actionpack (= 6.1.7.4) + activejob (= 6.1.7.4) + activerecord (= 6.1.7.4) + activesupport (= 6.1.7.4) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.7.1) + activesupport (6.1.7.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.1) + addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) airbrussh (1.4.1) @@ -85,31 +92,31 @@ GEM activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - attr_encrypted (3.1.0) + attr_encrypted (4.0.0) encryptor (~> 3.0.0) attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.670.0) - aws-sdk-core (3.168.2) + aws-partitions (1.780.0) + aws-sdk-core (3.175.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.60.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.67.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.117.2) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.126.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.5.2) aws-eventstream (~> 1, >= 1.0.2) - bcrypt (3.1.17) - better_errors (2.9.1) - coderay (>= 1.0.0) + bcrypt (3.1.18) + better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) + rouge (>= 1.0.0) better_html (2.0.1) actionview (>= 6.0) activesupport (>= 6.0) @@ -117,32 +124,27 @@ GEM erubi (~> 1.4) parser (>= 2.4) smart_properties - bindata (2.4.10) + bindata (2.4.15) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - blurhash (0.1.6) - ffi (~> 1.14) - bootsnap (1.15.0) + blurhash (0.1.7) + bootsnap (1.16.0) msgpack (~> 1.2) - brakeman (5.4.0) - browser (4.2.0) + brakeman (5.4.1) + browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) builder (3.2.4) - bullet (7.0.7) - activesupport (>= 3.0.0) - uniform_notifier (~> 1.11) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - byebug (11.1.3) - capistrano (3.17.1) + capistrano (3.17.3) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (2.0.1) + capistrano-bundler (2.1.0) capistrano (~> 3.1) capistrano-rails (1.6.2) capistrano (~> 3.1) @@ -152,7 +154,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.38.0) + capybara (3.39.2) addressable matrix mini_mime (>= 0.1.3) @@ -165,36 +167,35 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.2.4) + chewy (7.3.2) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl chunky_png (1.4.0) climate_control (0.2.0) cocoon (1.2.15) - coderay (1.1.3) color_diff (0.1) - concurrent-ruby (1.1.10) - connection_pool (2.3.0) - cose (1.2.1) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + cose (1.3.0) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) crack (0.4.5) rexml crass (1.0.6) - css_parser (1.12.0) + css_parser (1.14.0) addressable date (3.3.3) - debug_inspector (1.0.0) - devise (4.8.1) + debug_inspector (1.1.0) + devise (4.9.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (4.0.2) + devise-two-factor (4.1.0) activesupport (< 7.1) - attr_encrypted (>= 1.3, < 4, != 2) + attr_encrypted (>= 1.3, < 5, != 2) devise (~> 4.0) railties (< 7.1) rotp (~> 6.0) @@ -207,7 +208,7 @@ GEM docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.6.2) + doorkeeper (5.6.6) railties (>= 5) dotenv (2.8.1) dotenv-rails (2.8.1) @@ -227,11 +228,11 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.95.0) + excon (0.100.0) fabrication (2.30.0) - faker (3.1.0) + faker (3.2.0) i18n (>= 1.8.11, < 2) - faraday (1.9.3) + faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -247,15 +248,15 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.3) - multipart-post (>= 1.2, < 3) + faraday-multipart (1.0.4) + multipart-post (~> 2) faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) fast_blank (1.0.1) - fastimage (2.2.6) + fastimage (2.2.7) ffi (1.15.5) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -273,32 +274,34 @@ GEM fog-json (>= 1.0) ipaddress (>= 0.8) formatador (0.3.0) - fugit (1.7.1) + fugit (1.8.1) et-orbi (~> 1, >= 1.2.7) raabro (~> 1.4) fuubar (2.5.1) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) - gitlab-omniauth-openid-connect (0.10.0) - addressable (~> 2.7) - omniauth (>= 1.9, < 3) - openid_connect (~> 1.2) - globalid (1.0.1) + globalid (1.1.0) activesupport (>= 5.0) - hamlit (2.13.0) + haml (6.1.1) temple (>= 0.8.2) thor tilt - hamlit-rails (0.2.3) - actionpack (>= 4.0.1) - activesupport (>= 4.0.1) - hamlit (>= 1.2.0) - railties (>= 4.0.1) + haml-rails (2.1.0) + actionpack (>= 5.1) + activesupport (>= 5.1) + haml (>= 4.0.6) + railties (>= 5.1) + haml_lint (0.45.0) + haml (>= 4.0, < 6.2) + parallel (~> 1.10) + rainbow + rubocop (>= 0.50.0) + sysexits (~> 1.1) hashdiff (1.0.1) hashie (5.0.0) hcaptcha (7.1.0) json - highline (2.0.3) + highline (2.1.0) hiredis (0.6.3) hkdf (0.3.0) htmlentities (4.3.4) @@ -315,7 +318,7 @@ GEM httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.12.0) + i18n (1.14.1) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -332,25 +335,26 @@ GEM ipaddress (0.8.3) jmespath (1.6.2) json (2.6.3) - json-canonicalization (0.3.0) - json-jwt (1.14.0) + json-canonicalization (0.3.2) + json-jwt (1.15.3) activesupport (>= 4.2) aes_key_wrap bindata - json-ld (3.2.3) + httpclient + json-ld (3.2.5) htmlentities (~> 4.3) - json-canonicalization (~> 0.3) + json-canonicalization (~> 0.3, >= 0.3.2) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) - rack (~> 2.2) - rdf (~> 3.2, >= 3.2.9) + rack (>= 2.2, < 4) + rdf (~> 3.2, >= 3.2.10) json-ld-preloaded (3.2.2) json-ld (~> 3.2) rdf (~> 3.2) - json-schema (3.0.0) + json-schema (4.0.0) addressable (>= 2.8) jsonapi-renderer (0.2.2) - jwt (2.5.0) + jwt (2.7.1) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -363,14 +367,14 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kt-paperclip (7.1.1) + kt-paperclip (7.2.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) marcel (~> 1.0.1) mime-types terrapin (~> 0.6.0) - launchy (2.5.0) - addressable (~> 2.7) + launchy (2.5.2) + addressable (~> 2.8) letter_opener (1.8.1) launchy (>= 2.2, < 3) letter_opener_web (2.0.0) @@ -387,10 +391,10 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.19.1) + loofah (2.21.3) crass (~> 1.0.2) - nokogiri (>= 1.5.9) - mail (2.8.0.1) + nokogiri (>= 1.12.0) + mail (2.8.1) mini_mime (>= 0.1.1) net-imap net-pop @@ -405,36 +409,33 @@ GEM method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2022.0105) + mime-types-data (3.2023.0218.1) mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.17.0) - msgpack (1.6.0) + mini_portile2 (2.8.2) + minitest (5.18.1) + msgpack (1.7.1) multi_json (1.15.0) - multipart-post (2.1.1) - net-imap (0.3.4) + multipart-post (2.3.0) + net-http (0.3.2) + uri + net-imap (0.3.6) date net-protocol - net-ldap (0.17.1) + net-ldap (0.18.0) net-pop (0.1.2) net-protocol - net-protocol (0.1.3) + net-protocol (0.2.1) timeout - net-scp (4.0.0.rc1) + net-scp (4.0.0) net-ssh (>= 2.6.5, < 8.0.0) net-smtp (0.3.3) net-protocol - net-ssh (7.0.1) - nio4r (2.5.8) - nokogiri (1.14.0) - mini_portile2 (~> 2.8.0) + net-ssh (7.1.0) + nio4r (2.5.9) + nokogiri (1.15.2) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - nsa (0.2.8) - activesupport (>= 4.2, < 7) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.13.23) + oj (3.15.0) omniauth (1.9.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -448,33 +449,37 @@ GEM omniauth-saml (1.10.3) omniauth (~> 1.3, >= 1.3.2) ruby-saml (~> 1.9) - openid_connect (1.3.0) + omniauth_openid_connect (0.6.1) + omniauth (>= 1.9, < 3) + openid_connect (~> 1.1) + openid_connect (1.4.2) activemodel attr_required (>= 1.0.0) - json-jwt (>= 1.5.0) - rack-oauth2 (>= 1.6.1) - swd (>= 1.0.0) + json-jwt (>= 1.15.0) + net-smtp + rack-oauth2 (~> 1.21) + swd (~> 1.3) tzinfo validate_email validate_url - webfinger (>= 1.0.1) - openssl (3.0.0) - openssl-signature_algorithm (1.2.1) - openssl (> 2.0, < 3.1) + webfinger (~> 1.2) + openssl (3.1.0) + openssl-signature_algorithm (1.3.0) + openssl (> 2.0) orm_adapter (0.5.0) - ox (2.14.13) - parallel (1.22.1) - parser (3.2.0.0) + ox (2.14.16) + parallel (1.23.0) + parser (3.2.2.3) ast (~> 2.4.1) + racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.4.5) - pghero (2.8.3) - activerecord (>= 5) - pkg-config (1.5.1) + pg (1.5.3) + pghero (3.3.3) + activerecord (>= 6) posix-spawn (0.3.15) - premailer (1.18.0) + premailer (1.21.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -483,50 +488,42 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) - pry (0.14.1) - coderay (~> 1.1) - method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) - pry-rails (0.3.9) - pry (>= 0.10.4) public_suffix (5.0.1) - puma (5.6.5) + puma (6.3.0) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.6.2) - rack (2.2.6.2) + racc (1.7.1) + rack (2.2.7) rack-attack (6.6.1) rack (>= 1.0, < 3) - rack-cors (1.1.1) + rack-cors (2.0.1) rack (>= 2.0.0) - rack-oauth2 (1.19.0) + rack-oauth2 (1.21.3) activesupport attr_required httpclient json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-proxy (0.7.0) + rack-proxy (0.7.6) rack - rack-test (2.0.2) + rack-test (2.1.0) rack (>= 1.3) - rails (6.1.7.1) - actioncable (= 6.1.7.1) - actionmailbox (= 6.1.7.1) - actionmailer (= 6.1.7.1) - actionpack (= 6.1.7.1) - actiontext (= 6.1.7.1) - actionview (= 6.1.7.1) - activejob (= 6.1.7.1) - activemodel (= 6.1.7.1) - activerecord (= 6.1.7.1) - activestorage (= 6.1.7.1) - activesupport (= 6.1.7.1) + rails (6.1.7.4) + actioncable (= 6.1.7.4) + actionmailbox (= 6.1.7.4) + actionmailer (= 6.1.7.4) + actionpack (= 6.1.7.4) + actiontext (= 6.1.7.4) + actionview (= 6.1.7.4) + activejob (= 6.1.7.4) + activemodel (= 6.1.7.4) + activerecord (= 6.1.7.4) + activestorage (= 6.1.7.4) + activesupport (= 6.1.7.4) bundler (>= 1.15.0) - railties (= 6.1.7.1) + railties (= 6.1.7.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -535,116 +532,118 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.4) - loofah (~> 2.19, >= 2.19.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) - rails-settings-cached (0.6.6) - rails (>= 4.2.0) - railties (6.1.7.1) - actionpack (= 6.1.7.1) - activesupport (= 6.1.7.1) + railties (6.1.7.4) + actionpack (= 6.1.7.4) + activesupport (= 6.1.7.4) method_source rake (>= 12.2) thor (~> 1.0) rainbow (3.1.1) rake (13.0.6) - rdf (3.2.9) + rdf (3.2.11) link_header (~> 0.0, >= 0.0.8) - rdf-normalize (0.5.1) + rdf-normalize (0.6.0) rdf (~> 3.2) - redcarpet (3.5.1) - redis (4.5.1) - redis-namespace (1.10.0) + redcarpet (3.6.0) + redis (4.8.1) + redis-namespace (1.11.0) redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.6.1) + regexp_parser (2.8.1) request_store (1.5.1) rack (>= 1.4) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) + responders (3.1.0) + actionpack (>= 5.2) + railties (>= 5.2) rexml (3.2.5) - rotp (6.2.0) + rotp (6.2.2) + rouge (4.1.2) rpam2 (4.0.2) - rqrcode (2.1.2) + rqrcode (2.2.0) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-rails (5.1.2) - actionpack (>= 5.2) - activesupport (>= 5.2) - railties (>= 5.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-support (~> 3.12.0) + rspec-rails (6.0.3) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) - rspec-support (3.11.1) - rspec_junit_formatter (0.6.0) - rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.43.0) + rspec-support (3.12.0) + rspec_chunked (0.6) + rubocop (1.52.1) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.24.1, < 2.0) + rubocop-ast (>= 1.28.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.24.1) - parser (>= 3.1.1.0) - rubocop-capybara (2.17.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.18.0) rubocop (~> 1.41) - rubocop-performance (1.15.2) + rubocop-factory_bot (2.23.1) + rubocop (~> 1.33) + rubocop-performance (1.18.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.17.4) + rubocop-rails (2.19.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) - rubocop-rspec (2.18.0) + rubocop-rspec (2.22.0) rubocop (~> 1.33) - rubocop-capybara - ruby-progressbar (1.11.0) - ruby-saml (1.13.0) - nokogiri (>= 1.10.5) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-progressbar (1.13.0) + ruby-saml (1.15.0) + nokogiri (>= 1.13.10) rexml ruby2_keywords (0.0.5) - rufus-scheduler (3.8.2) + rubyzip (2.3.2) + rufus-scheduler (3.9.1) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) - sanitize (6.0.0) + sanitize (6.0.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) scenic (1.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) semantic_range (3.0.0) - sidekiq (6.5.8) + sidekiq (6.5.9) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (4.0.3) - redis (>= 4.2.0) + sidekiq-scheduler (5.0.3) rufus-scheduler (~> 3.2) - sidekiq (>= 4, < 7) + sidekiq (>= 6, < 8) tilt (>= 1.4.0) sidekiq-unique-jobs (7.1.29) brpoplpush-redis_script (> 0.1.1, <= 2.0.0) @@ -654,7 +653,7 @@ GEM thor (>= 0.20, < 3.0) simple-navigation (4.4.0) activesupport (>= 2.3.2) - simple_form (5.1.0) + simple_form (5.2.0) actionpack (>= 5.2) activemodel (>= 5.2) simplecov (0.22.0) @@ -671,30 +670,30 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - sshkit (1.21.2) + sshkit (1.21.4) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.23) - statsd-ruby (1.5.0) + stackprof (0.2.25) stoplight (3.0.1) redlock (~> 1.0) - strong_migrations (0.7.9) - activerecord (>= 5) + strong_migrations (0.8.0) + activerecord (>= 5.2) swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) httpclient (>= 2.4) - temple (0.8.2) + sysexits (1.2.0) + temple (0.10.2) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - thor (1.2.1) - tilt (2.0.11) - timeout (0.3.0) - tpm-key_attestation (0.11.0) + thor (1.2.2) + tilt (2.2.0) + timeout (0.3.2) + tpm-key_attestation (0.12.0) bindata (~> 2.4) - openssl (> 2.0, < 3.1) + openssl (> 2.0) openssl-signature_algorithm (~> 1.0) tty-color (0.6.0) tty-cursor (0.7.1) @@ -709,15 +708,15 @@ GEM twitter-text (3.1.0) idn-ruby unf (~> 0.1.0) - tzinfo (2.0.5) + tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2022.7) + tzinfo-data (1.2023.3) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - uniform_notifier (1.16.0) + uri (0.12.2) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -726,15 +725,15 @@ GEM public_suffix warden (1.2.9) rack (>= 2.0.9) - webauthn (2.5.2) + webauthn (3.0.0) android_key_attestation (~> 0.3.0) awrence (~> 1.1) bindata (~> 2.4) cbor (~> 0.5.9) cose (~> 1.1) - openssl (>= 2.2, < 3.1) + openssl (>= 2.2) safety_net_attestation (~> 0.4.0) - tpm-key_attestation (~> 0.11.0) + tpm-key_attestation (~> 0.12.0) webfinger (1.2.0) activesupport httpclient (>= 2.4) @@ -742,7 +741,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.4.3) + webpacker (5.4.4) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) @@ -754,53 +753,51 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.6) + zeitwerk (2.6.8) PLATFORMS ruby DEPENDENCIES active_model_serializers (~> 0.10) - active_record_query_trace (~> 1.8) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.117) + aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) - bootsnap (~> 1.15.0) + bootsnap (~> 1.16.0) brakeman (~> 5.4) browser - bullet (~> 7.0) bundler-audit (~> 0.9) capistrano (~> 3.17) capistrano-rails (~> 1.6) capistrano-rbenv (~> 2.2) capistrano-yarn (~> 2.0) - capybara (~> 3.38) + capybara (~> 3.39) charlock_holmes (~> 0.7.7) - chewy (~> 7.2) + chewy (~> 7.3) climate_control (~> 0.2) cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby connection_pool - devise (~> 4.8) - devise-two-factor (~> 4.0) + devise (~> 4.9) + devise-two-factor (~> 4.1) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) doorkeeper (~> 5.6) dotenv-rails (~> 2.8) ed25519 (~> 1.3) fabrication (~> 2.30) - faker (~> 3.1) + faker (~> 3.2) fast_blank (~> 1.0) fastimage fog-core (<= 2.4.0) fog-openstack (~> 0.3) fuubar (~> 2.5) - gitlab-omniauth-openid-connect (~> 0.10.0) - hamlit-rails (~> 0.2) + haml-rails (~> 2.0) + haml_lint hcaptcha (~> 7.1) hiredis (~> 0.6) htmlentities (~> 4.3) @@ -811,9 +808,9 @@ DEPENDENCIES idn-ruby json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 3.0) + json-schema (~> 4.0) kaminari (~> 1.2) - kt-paperclip (~> 7.1) + kt-paperclip (~> 7.2) letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) @@ -822,69 +819,74 @@ DEPENDENCIES mario-redis-lock (~> 1.2) memory_profiler mime-types (~> 3.4.1) - net-ldap (~> 0.17) - nokogiri (~> 1.14) - nsa (~> 0.2) - oj (~> 3.13) + net-http (~> 0.3.2) + net-ldap (~> 0.18) + nokogiri (~> 1.15) + oj (~> 3.14) omniauth (~> 1.9) omniauth-cas (~> 2.0) omniauth-rails_csrf_protection (~> 0.1) omniauth-saml (~> 1.10) + omniauth_openid_connect (~> 0.6.1) ox (~> 2.14) parslet - pg (~> 1.4) - pghero (~> 2.8) - pkg-config (~> 1.5) + pg (~> 1.5) + pghero posix-spawn premailer-rails private_address_check (~> 0.5) - pry-byebug (~> 3.10) - pry-rails (~> 0.3) public_suffix (~> 5.0) - puma (~> 5.6) + puma (~> 6.3) pundit (~> 2.3) - rack (~> 2.2.6) + rack (~> 2.2.7) rack-attack (~> 6.6) - rack-cors (~> 1.1) - rack-test (~> 2.0) + rack-cors (~> 2.0) + rack-test (~> 2.1) rails (~> 6.1.7) rails-controller-testing (~> 1.0) rails-i18n (~> 6.0) - rails-settings-cached (~> 0.6) + rails-settings-cached (~> 0.6)! rdf-normalize (~> 0.5) - redcarpet (~> 3.5) + redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) - rexml (~> 3.2) - rqrcode (~> 2.1) - rspec-rails (~> 5.1) + rqrcode (~> 2.2) + rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) - rspec_junit_formatter (~> 0.6) + rspec_chunked (~> 0.6) rubocop + rubocop-capybara rubocop-performance rubocop-rails rubocop-rspec - ruby-progressbar (~> 1.11) + ruby-progressbar (~> 1.13) + rubyzip (~> 2.3) sanitize (~> 6.0) scenic (~> 1.7) sidekiq (~> 6.5) sidekiq-bulk (~> 0.2.0) - sidekiq-scheduler (~> 4.0) + sidekiq-scheduler (~> 5.0) sidekiq-unique-jobs (~> 7.1) simple-navigation (~> 4.4) - simple_form (~> 5.1) + simple_form (~> 5.2) simplecov (~> 0.22) sprockets (~> 3.7.2) sprockets-rails (~> 3.4) stackprof stoplight (~> 3.0.1) - strong_migrations (~> 0.7) + strong_migrations (~> 0.8) thor (~> 1.2) tty-prompt (~> 0.23) twitter-text (~> 3.1.0) - tzinfo-data (~> 1.2022) - webauthn (~> 2.5) + tzinfo-data (~> 1.2023) + webauthn (~> 3.0) webmock (~> 3.18) webpacker (~> 5.4) webpush! xorcist (~> 1.1) + +RUBY VERSION + ruby 3.2.2p53 + +BUNDLED WITH + 2.4.13 diff --git a/README.md b/README.md index 256f2d2def..f878752fe3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Mastodon Glitch Edition # +# Mastodon Glitch Edition -> Now with automated deploys! +> Now with automated deploys! [![Build Status](https://img.shields.io/circleci/project/github/glitch-soc/mastodon.svg)][circleci] [![Code Climate](https://img.shields.io/codeclimate/maintainability/glitch-soc/mastodon.svg)][code_climate] diff --git a/SECURITY.md b/SECURITY.md index ccc7c10346..6a51c126ab 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can reach us at . -You should *not* report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. +You should _not_ report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. ## Scope @@ -11,7 +11,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through ## Supported Versions | Version | Supported | -| ------- | ----------| +| ------- | --------- | +| 4.1.x | Yes | | 4.0.x | Yes | | 3.5.x | Yes | | < 3.5 | No | diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index e38e14a106..abde8e92f1 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -2,8 +2,37 @@ class AccountsIndex < Chewy::Index settings index: { refresh_interval: '30s' }, analysis: { + filter: { + english_stop: { + type: 'stop', + stopwords: '_english_', + }, + + english_stemmer: { + type: 'stemmer', + language: 'english', + }, + + english_possessive_stemmer: { + type: 'stemmer', + language: 'possessive_english', + }, + }, + analyzer: { - content: { + natural: { + tokenizer: 'uax_url_email', + filter: %w( + english_possessive_stemmer + lowercase + asciifolding + cjk_width + english_stop + english_stemmer + ), + }, + + verbatim: { tokenizer: 'whitespace', filter: %w(lowercase asciifolding cjk_width), }, @@ -26,18 +55,13 @@ class AccountsIndex < Chewy::Index index_scope ::Account.searchable.includes(:account_stat) root date_detection: false do - field :id, type: 'long' - - field :display_name, type: 'text', analyzer: 'content' do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do - field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' - end - - field :following_count, type: 'long', value: ->(account) { account.following_count } - field :followers_count, type: 'long', value: ->(account) { account.followers_count } - field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } + field(:id, type: 'long') + field(:following_count, type: 'long') + field(:followers_count, type: 'long') + field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties }) + field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }) + field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' } + field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' } end end diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 1043486140..c4b7e9c9d2 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -8,7 +8,7 @@ class AboutController < ApplicationController before_action :set_instance_presenter def show - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 4d03a04b77..929bb54aa7 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,8 +7,9 @@ class AccountsController < ApplicationController include AccountControllerConcern include SignatureAuthentication + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -16,7 +17,7 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? @rss_url = rss_url end diff --git a/app/controllers/activitypub/base_controller.rb b/app/controllers/activitypub/base_controller.rb index b8a7e0ab96..388d4b9e1d 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -7,10 +7,6 @@ class ActivityPub::BaseController < Api::BaseController private - def set_cache_headers - response.headers['Vary'] = 'Signature' if authorized_fetch_mode? - end - def skip_temporary_suspension_response? false end diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 23d8740711..4ed59388ff 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,11 +4,12 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_items before_action :set_size before_action :set_type - before_action :set_cache_headers def show expires_in 3.minutes, public: public_fetch_mode? diff --git a/app/controllers/activitypub/followers_synchronizations_controller.rb b/app/controllers/activitypub/followers_synchronizations_controller.rb index 4e445bcb1f..976caa3445 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -4,9 +4,10 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature! before_action :set_items - before_action :set_cache_headers def show expires_in 0, public: false diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 60d201f763..bf10ba762a 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -6,9 +6,10 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController include SignatureVerification include AccountOwnedConcern + vary_by -> { 'Signature' if authorized_fetch_mode? || page_requested? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_statuses - before_action :set_cache_headers def show if page_requested? @@ -16,6 +17,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController else expires_in(3.minutes, public: public_fetch_mode?) end + render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end @@ -80,8 +82,4 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController def set_account @account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative end - - def set_cache_headers - response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested? - end end diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 8e0f9de2ee..c38ff89d1c 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -7,9 +7,10 @@ class ActivityPub::RepliesController < ActivityPub::BaseController DESCENDANTS_LIMIT = 60 + vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :require_account_signature!, if: :authorized_fetch_mode? before_action :set_status - before_action :set_cache_headers before_action :set_replies def index diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb index 351b9a9910..8f9708183a 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -14,6 +14,10 @@ class Admin::AnnouncementsController < Admin::BaseController @announcement = Announcement.new end + def edit + authorize :announcement, :update? + end + def create authorize :announcement, :create? @@ -28,10 +32,6 @@ class Admin::AnnouncementsController < Admin::BaseController end end - def edit - authorize :announcement, :update? - end - def update authorize :announcement, :update? diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index c645ce12bb..a71bb61298 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -9,6 +9,8 @@ module Admin before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers + after_action :verify_authorized private @@ -21,6 +23,10 @@ module Admin use_pack 'admin' end + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + def set_user @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 924b623ad8..3a6df662ea 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -14,17 +14,5 @@ module Admin @pending_tags_count = Tag.pending_review.count @pending_appeals_count = Appeal.pending.count end - - private - - def redis_info - @redis_info ||= begin - if redis.is_a?(Redis::Namespace) - redis.redis.info - else - redis.info - end - end - end end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 74764640b8..b9691c5a3a 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -2,7 +2,7 @@ module Admin class DomainBlocksController < BaseController - before_action :set_domain_block, only: [:show, :destroy, :edit, :update] + before_action :set_domain_block, only: [:destroy, :edit, :update] def batch authorize :domain_block, :create? @@ -31,31 +31,41 @@ module Admin @domain_block = DomainBlock.new(resource_params) existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil + # Disallow accidentally downgrading a domain block if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save - flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe # rubocop:disable Rails/OutputSafety + flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe @domain_block.errors.delete(:domain) - render :new - else - if existing_domain_block.present? - @domain_block = existing_domain_block - @domain_block.update(resource_params) - end + return render :new + end - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id) - log_action :create, @domain_block - redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') - else - render :new - end + # Allow transparently upgrading a domain block + if existing_domain_block.present? + @domain_block = existing_domain_block + @domain_block.assign_attributes(resource_params) + end + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + else + render :new end end def update authorize :domain_block, :update? - if @domain_block.update(update_params) + @domain_block.assign_attributes(update_params) + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') @@ -90,9 +100,11 @@ module Admin end def action_from_button - if params[:save] - 'save' - end + 'save' if params[:save] + end + + def requires_confirmation? + @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm] end end end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index a0a43de192..4a3228ec30 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -2,8 +2,6 @@ module Admin class EmailDomainBlocksController < BaseController - before_action :set_email_domain_block, only: [:show, :destroy] - def index authorize :email_domain_block, :index? @@ -59,10 +57,6 @@ module Admin private - def set_email_domain_block - @email_domain_block = EmailDomainBlock.find(params[:id]) - end - def set_resolved_records Resolv::DNS.open do |dns| dns.timeouts = 5 diff --git a/app/controllers/admin/roles_controller.rb b/app/controllers/admin/roles_controller.rb index d76aa745bd..bcfc11159c 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -16,6 +16,10 @@ module Admin @role = UserRole.new end + def edit + authorize @role, :update? + end + def create authorize :user_role, :create? @@ -30,10 +34,6 @@ module Admin end end - def edit - authorize @role, :update? - end - def update authorize @role, :update? diff --git a/app/controllers/admin/rules_controller.rb b/app/controllers/admin/rules_controller.rb index f3bed3ad8e..d31aec6ea8 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -11,6 +11,10 @@ module Admin @rule = Rule.new end + def edit + authorize @rule, :update? + end + def create authorize :rule, :create? @@ -24,10 +28,6 @@ module Admin end end - def edit - authorize @rule, :update? - end - def update authorize @rule, :update? diff --git a/app/controllers/admin/warning_presets_controller.rb b/app/controllers/admin/warning_presets_controller.rb index b376f8d9b1..efbf65b119 100644 --- a/app/controllers/admin/warning_presets_controller.rb +++ b/app/controllers/admin/warning_presets_controller.rb @@ -11,6 +11,10 @@ module Admin @warning_preset = AccountWarningPreset.new end + def edit + authorize @warning_preset, :update? + end + def create authorize :account_warning_preset, :create? @@ -24,10 +28,6 @@ module Admin end end - def edit - authorize @warning_preset, :update? - end - def update authorize @warning_preset, :update? diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index d6fb1a4eaf..f1aad7c4b5 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -10,16 +10,25 @@ module Admin @webhooks = Webhook.page(params[:page]) end + def show + authorize @webhook, :show? + end + def new authorize :webhook, :create? @webhook = Webhook.new end + def edit + authorize @webhook, :update? + end + def create authorize :webhook, :create? @webhook = Webhook.new(resource_params) + @webhook.current_account = current_account if @webhook.save redirect_to admin_webhook_path(@webhook) @@ -28,21 +37,15 @@ module Admin end end - def show - authorize @webhook, :show? - end - - def edit - authorize @webhook, :update? - end - def update authorize @webhook, :update? + @webhook.current_account = current_account + if @webhook.update(resource_params) redirect_to admin_webhook_path(@webhook) else - render :show + render :edit end end @@ -71,7 +74,7 @@ module Admin end def resource_params - params.require(:webhook).permit(:url, events: []) + params.require(:webhook).permit(:url, :template, events: []) end end end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 41f3ce2ee3..2629ab782f 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -6,13 +6,14 @@ class Api::BaseController < ApplicationController include RateLimitHeaders include AccessTokenTrackingConcern + include ApiCachingConcern - skip_before_action :store_current_location skip_before_action :require_functional!, unless: :whitelist_mode? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_not_suspended! - before_action :set_cache_headers + + vary_by 'Authorization' protect_from_forgery with: :null_session @@ -148,10 +149,6 @@ class Api::BaseController < ApplicationController doorkeeper_authorize!(*scopes) if doorkeeper_token end - def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' - end - def disallow_unauthenticated_api_access? ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode end diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 94b707771f..7c7d70fd32 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -13,7 +13,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def update @account = current_account UpdateAccountService.new.call(@account, account_params, raise_error: true) - UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params + current_user.update(user_params) if user_params ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end @@ -34,15 +34,17 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController ) end - def user_settings_params + def user_params return nil if params[:source].blank? source_params = params.require(:source) { - 'setting_default_privacy' => source_params.fetch(:privacy, @account.user.setting_default_privacy), - 'setting_default_sensitive' => source_params.fetch(:sensitive, @account.user.setting_default_sensitive), - 'setting_default_language' => source_params.fetch(:language, @account.user.setting_default_language), + settings_attributes: { + default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy), + default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive), + default_language: source_params.fetch(:language, @account.user.setting_default_language), + }, } end end diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index b61de13b91..1a996d362a 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end @@ -45,15 +46,11 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController end def next_path - if records_continue? - api_v1_account_followers_url pagination_params(max_id: pagination_max_id) - end + api_v1_account_followers_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_account_followers_url pagination_params(since_id: pagination_since_id) - end + api_v1_account_followers_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 37d3c2d783..6e6ebae43b 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end @@ -45,15 +46,11 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController end def next_path - if records_continue? - api_v1_account_following_index_url pagination_params(max_id: pagination_max_id) - end + api_v1_account_following_index_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_account_following_index_url pagination_params(since_id: pagination_since_id) - end + api_v1_account_following_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb index 8597f891d6..6d63398781 100644 --- a/app/controllers/api/v1/accounts/lookup_controller.rb +++ b/app/controllers/api/v1/accounts/lookup_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController before_action :set_account def show + cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 38c9f5a20d..51f541bd23 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -7,6 +7,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) } def index + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end @@ -39,15 +40,11 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def next_path - if records_continue? - api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) - end + api_v1_account_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @statuses.empty? - api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) - end + api_v1_account_statuses_url pagination_params(min_id: pagination_since_id) unless @statuses.empty? end def records_continue? diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index be84720aa9..ddb94d5ca4 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -18,6 +18,7 @@ class Api::V1::AccountsController < Api::BaseController override_rate_limit_headers :follow, family: :follows def show + cache_if_unauthenticated! render json: @account, serializer: REST::AccountSerializer end @@ -30,7 +31,7 @@ class Api::V1::AccountsController < Api::BaseController self.response_body = Oj.dump(response.body) self.status = response.status rescue ActiveRecord::RecordInvalid => e - render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: :unprocessable_entity + render json: ValidationErrorFormatter.new(e, 'account.username': :username, 'invite_request.text': :reason).as_json, status: 422 end def follow @@ -89,7 +90,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason) + params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone) end def check_enabled_registrations diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb index f483000728..ff9cae6398 100644 --- a/app/controllers/api/v1/admin/accounts_controller.rb +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -120,9 +120,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController translated_params[:status] = status.to_s if params[status].present? end - if params[:staff].present? - translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id) - end + translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id) if params[:staff].present? translated_params end diff --git a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb index 9ef1b3be71..7b192b979f 100644 --- a/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb +++ b/app/controllers/api/v1/admin/canonical_email_blocks_controller.rb @@ -58,7 +58,7 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController end def set_canonical_email_blocks_from_test - @canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email]) + @canonical_email_blocks = CanonicalEmailBlock.matching_email(params.require(:email)) end def set_canonical_email_block diff --git a/app/controllers/api/v1/admin/domain_allows_controller.rb b/app/controllers/api/v1/admin/domain_allows_controller.rb index 0658199f0f..dd54d67106 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -16,19 +16,6 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController PAGINATION_PARAMS = %i(limit).freeze - def create - authorize :domain_allow, :create? - - @domain_allow = DomainAllow.find_by(resource_params) - - if @domain_allow.nil? - @domain_allow = DomainAllow.create!(resource_params) - log_action :create, @domain_allow - end - - render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer - end - def index authorize :domain_allow, :index? render json: @domain_allows, each_serializer: REST::Admin::DomainAllowSerializer @@ -39,6 +26,19 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer end + def create + authorize :domain_allow, :create? + + @domain_allow = DomainAllow.find_by(domain: resource_params[:domain]) + + if @domain_allow.nil? + @domain_allow = DomainAllow.create!(resource_params) + log_action :create, @domain_allow + end + + render json: @domain_allow, serializer: REST::Admin::DomainAllowSerializer + end + def destroy authorize @domain_allow, :destroy? UnallowDomainService.new.call(@domain_allow) diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index 8b77e9717d..2538c7c7c2 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -16,6 +16,16 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController PAGINATION_PARAMS = %i(limit).freeze + def index + authorize :domain_block, :index? + render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer + end + + def show + authorize @domain_block, :show? + render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer + end + def create authorize :domain_block, :create? @@ -28,16 +38,6 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer end - def index - authorize :domain_block, :index? - render json: @domain_blocks, each_serializer: REST::Admin::DomainBlockSerializer - end - - def show - authorize @domain_block, :show? - render json: @domain_block, serializer: REST::Admin::DomainBlockSerializer - end - def update authorize @domain_block, :update? @domain_block.update!(domain_block_params) diff --git a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb index e53d0b1573..850eda6224 100644 --- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -18,15 +18,6 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController limit ).freeze - def create - authorize :email_domain_block, :create? - - @email_domain_block = EmailDomainBlock.create!(resource_params) - log_action :create, @email_domain_block - - render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer - end - def index authorize :email_domain_block, :index? render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer @@ -37,6 +28,15 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer end + def create + authorize :email_domain_block, :create? + + @email_domain_block = EmailDomainBlock.create!(resource_params) + log_action :create, @email_domain_block + + render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer + end + def destroy authorize @email_domain_block, :destroy? @email_domain_block.destroy! diff --git a/app/controllers/api/v1/admin/ip_blocks_controller.rb b/app/controllers/api/v1/admin/ip_blocks_controller.rb index 201ab6b1ff..61c1912344 100644 --- a/app/controllers/api/v1/admin/ip_blocks_controller.rb +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -18,13 +18,6 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController limit ).freeze - def create - authorize :ip_block, :create? - @ip_block = IpBlock.create!(resource_params) - log_action :create, @ip_block - render json: @ip_block, serializer: REST::Admin::IpBlockSerializer - end - def index authorize :ip_block, :index? render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer @@ -35,6 +28,13 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController render json: @ip_block, serializer: REST::Admin::IpBlockSerializer end + def create + authorize :ip_block, :create? + @ip_block = IpBlock.create!(resource_params) + log_action :create, @ip_block + render json: @ip_block, serializer: REST::Admin::IpBlockSerializer + end + def update authorize @ip_block, :update? @ip_block.update(resource_params) diff --git a/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb new file mode 100644 index 0000000000..5d9fcc82c0 --- /dev/null +++ b/app/controllers/api/v1/admin/trends/links/preview_card_providers_controller.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseController + include Authorization + + LIMIT = 100 + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + before_action :set_providers, only: :index + + after_action :verify_authorized + after_action :insert_pagination_headers, only: :index + + PAGINATION_PARAMS = %i(limit).freeze + + def index + authorize :preview_card_provider, :index? + + render json: @providers, each_serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + def approve + authorize :preview_card_provider, :review? + + provider = PreviewCardProvider.find(params[:id]) + provider.update(trendable: true, reviewed_at: Time.now.utc) + render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + def reject + authorize :preview_card_provider, :review? + + provider = PreviewCardProvider.find(params[:id]) + provider.update(trendable: false, reviewed_at: Time.now.utc) + render json: provider, serializer: REST::Admin::Trends::Links::PreviewCardProviderSerializer + end + + private + + def set_providers + @providers = PreviewCardProvider.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_trends_links_preview_card_providers_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty? + end + + def pagination_max_id + @providers.last.id + end + + def pagination_since_id + @providers.first.id + end + + def records_continue? + @providers.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb index cc63889806..7f4ca48288 100644 --- a/app/controllers/api/v1/admin/trends/links_controller.rb +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::LinksController < Api::V1::Trends::LinksController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index + + def index + if current_user&.can?(:manage_taxonomies) + render json: @links, each_serializer: REST::Admin::Trends::LinkSerializer + else + super + end + end + + def approve + authorize :preview_card, :review? + + link = PreviewCard.find(params[:id]) + link.update(trendable: true) + render json: link, serializer: REST::Admin::Trends::LinkSerializer + end + + def reject + authorize :preview_card, :review? + + link = PreviewCard.find(params[:id]) + link.update(trendable: false) + render json: link, serializer: REST::Admin::Trends::LinkSerializer + end private diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb index c39f77363c..34b6580df1 100644 --- a/app/controllers/api/v1/admin/trends/statuses_controller.rb +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -1,7 +1,36 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::StatusesController < Api::V1::Trends::StatusesController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index + + def index + if current_user&.can?(:manage_taxonomies) + render json: @statuses, each_serializer: REST::Admin::Trends::StatusSerializer + else + super + end + end + + def approve + authorize [:admin, :status], :review? + + status = Status.find(params[:id]) + status.update(trendable: true) + render json: status, serializer: REST::Admin::Trends::StatusSerializer + end + + def reject + authorize [:admin, :status], :review? + + status = Status.find(params[:id]) + status.update(trendable: false) + render json: status, serializer: REST::Admin::Trends::StatusSerializer + end private diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index e77df30216..2eeea95225 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController - before_action -> { authorize_if_got_token! :'admin:read' } + include Authorization + + before_action -> { authorize_if_got_token! :'admin:read' }, only: :index + before_action -> { authorize_if_got_token! :'admin:write' }, except: :index + + after_action :verify_authorized, except: :index def index if current_user&.can?(:manage_taxonomies) @@ -11,6 +16,22 @@ class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController end end + def approve + authorize :tag, :review? + + tag = Tag.find(params[:id]) + tag.update(trendable: true, reviewed_at: Time.now.utc) + render json: tag, serializer: REST::Admin::TagSerializer + end + + def reject + authorize :tag, :review? + + tag = Tag.find(params[:id]) + tag.update(trendable: false, reviewed_at: Time.now.utc) + render json: tag, serializer: REST::Admin::TagSerializer + end + private def enabled? diff --git a/app/controllers/api/v1/announcements_controller.rb b/app/controllers/api/v1/announcements_controller.rb index ee79fc19f1..82e9cf7de4 100644 --- a/app/controllers/api/v1/announcements_controller.rb +++ b/app/controllers/api/v1/announcements_controller.rb @@ -18,9 +18,7 @@ class Api::V1::AnnouncementsController < Api::BaseController private def set_announcements - @announcements = begin - Announcement.published.chronological - end + @announcements = Announcement.published.chronological end def set_announcement diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index a65e762c9f..06a8bfa891 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -33,15 +33,11 @@ class Api::V1::BlocksController < Api::BaseController end def next_path - if records_continue? - api_v1_blocks_url pagination_params(max_id: pagination_max_id) - end + api_v1_blocks_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless paginated_blocks.empty? - api_v1_blocks_url pagination_params(since_id: pagination_since_id) - end + api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 6c75834037..b3ca2f7903 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController def index @conversations = paginated_conversations - render json: @conversations, each_serializer: REST::ConversationSerializer + render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id) end def read @@ -19,6 +19,11 @@ class Api::V1::ConversationsController < Api::BaseController render json: @conversation, serializer: REST::ConversationSerializer end + def unread + @conversation.update!(unread: true) + render json: @conversation, serializer: REST::ConversationSerializer + end + def destroy @conversation.destroy! render_empty @@ -32,6 +37,19 @@ class Api::V1::ConversationsController < Api::BaseController def paginated_conversations AccountConversation.where(account: current_account) + .includes( + account: :account_stat, + last_status: [ + :media_attachments, + :preview_cards, + :status_stat, + :tags, + { + active_mentions: [account: :account_stat], + account: :account_stat, + }, + ] + ) .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) end @@ -40,15 +58,11 @@ class Api::V1::ConversationsController < Api::BaseController end def next_path - if records_continue? - api_v1_conversations_url pagination_params(max_id: pagination_max_id) - end + api_v1_conversations_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @conversations.empty? - api_v1_conversations_url pagination_params(min_id: pagination_since_id) - end + api_v1_conversations_url pagination_params(min_id: pagination_since_id) unless @conversations.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 3a7db929bf..ef2024b6b1 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class Api::V1::CustomEmojisController < Api::BaseController - skip_before_action :set_cache_headers + vary_by '', unless: :disallow_unauthenticated_api_access? def index - expires_in 3.minutes, public: true + cache_even_if_authenticated! unless disallow_unauthenticated_api_access? render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.local.where(disabled: false).includes(:category) } end end diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c91543e3a3..1109435507 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -5,6 +5,7 @@ class Api::V1::DirectoriesController < Api::BaseController before_action :set_accounts def show + cache_if_unauthenticated! render json: @accounts, each_serializer: REST::AccountSerializer end @@ -20,11 +21,35 @@ class Api::V1::DirectoriesController < Api::BaseController def accounts_scope Account.discoverable.tap do |scope| - scope.merge!(Account.local) if truthy_param?(:local) - scope.merge!(Account.by_recent_status) if params[:order].blank? || params[:order] == 'active' - scope.merge!(Account.order(id: :desc)) if params[:order] == 'new' - scope.merge!(Account.not_excluded_by_account(current_account)) if current_account - scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local) + scope.merge!(account_order_scope) + scope.merge!(local_account_scope) if local_accounts? + scope.merge!(account_exclusion_scope) if current_account + scope.merge!(account_domain_block_scope) if current_account && !local_accounts? end end + + def local_accounts? + truthy_param?(:local) + end + + def account_order_scope + case params[:order] + when 'new' + Account.order(id: :desc) + when 'active', nil + Account.by_recent_status + end + end + + def local_account_scope + Account.local + end + + def account_exclusion_scope + Account.not_excluded_by_account(current_account) + end + + def account_domain_block_scope + Account.not_domain_blocked_by_account(current_account) + end end diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb index 1891261b9c..34def3c44a 100644 --- a/app/controllers/api/v1/domain_blocks_controller.rb +++ b/app/controllers/api/v1/domain_blocks_controller.rb @@ -43,15 +43,11 @@ class Api::V1::DomainBlocksController < Api::BaseController end def next_path - if records_continue? - api_v1_domain_blocks_url pagination_params(max_id: pagination_max_id) - end + api_v1_domain_blocks_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @blocks.empty? - api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) - end + api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb index 3faaea2fb7..16e91b4497 100644 --- a/app/controllers/api/v1/emails/confirmations_controller.rb +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true class Api::V1::Emails::ConfirmationsController < Api::BaseController - before_action -> { doorkeeper_authorize! :write, :'write:accounts' } - before_action :require_user_owned_by_application! - before_action :require_user_not_confirmed! + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :check + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :check + before_action :require_user_owned_by_application!, except: :check + before_action :require_user_not_confirmed!, except: :check + before_action :require_authenticated_user!, only: :check def create current_user.update!(email: params[:email]) if params.key?(:email) @@ -12,13 +14,17 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController render_empty end + def check + render json: current_user.confirmed? + end + private def require_user_owned_by_application! - render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: :forbidden unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id + render json: { error: 'This method is only available to the application the user originally signed-up with' }, status: 403 unless current_user && current_user.created_by_application_id == doorkeeper_token.application_id end def require_user_not_confirmed! - render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: :forbidden unless !current_user.confirmed? || current_user.unconfirmed_email.present? + render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: 403 unless !current_user.confirmed? || current_user.unconfirmed_email.present? end end diff --git a/app/controllers/api/v1/endorsements_controller.rb b/app/controllers/api/v1/endorsements_controller.rb index 9e80f468a7..46e3fcd647 100644 --- a/app/controllers/api/v1/endorsements_controller.rb +++ b/app/controllers/api/v1/endorsements_controller.rb @@ -35,17 +35,13 @@ class Api::V1::EndorsementsController < Api::BaseController def next_path return if unlimited? - if records_continue? - api_v1_endorsements_url pagination_params(max_id: pagination_max_id) - end + api_v1_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path return if unlimited? - unless @accounts.empty? - api_v1_endorsements_url pagination_params(since_id: pagination_since_id) - end + api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index 2a873696c0..bd7f3d775e 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -36,15 +36,11 @@ class Api::V1::FavouritesController < Api::BaseController end def next_path - if records_continue? - api_v1_favourites_url pagination_params(max_id: pagination_max_id) - end + api_v1_favourites_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless results.empty? - api_v1_favourites_url pagination_params(min_id: pagination_since_id) - end + api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb index edb42a94ea..516046f009 100644 --- a/app/controllers/api/v1/featured_tags_controller.rb +++ b/app/controllers/api/v1/featured_tags_controller.rb @@ -13,7 +13,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController end def create - featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name]) + featured_tag = CreateFeaturedTagService.new.call(current_account, params.require(:name)) render json: featured_tag, serializer: REST::FeaturedTagSerializer end @@ -31,8 +31,4 @@ class Api::V1::FeaturedTagsController < Api::BaseController def set_featured_tags @featured_tags = current_account.featured_tags.order(statuses_count: :desc) end - - def featured_tag_params - params.permit(:name) - end end diff --git a/app/controllers/api/v1/filters_controller.rb b/app/controllers/api/v1/filters_controller.rb index 772791b255..ed98acce30 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -11,6 +11,10 @@ class Api::V1::FiltersController < Api::BaseController render json: @filters, each_serializer: REST::V1::FilterSerializer end + def show + render json: @filter, serializer: REST::V1::FilterSerializer + end + def create ApplicationRecord.transaction do filter_category = current_account.custom_filters.create!(filter_params) @@ -20,10 +24,6 @@ class Api::V1::FiltersController < Api::BaseController render json: @filter, serializer: REST::V1::FilterSerializer end - def show - render json: @filter, serializer: REST::V1::FilterSerializer - end - def update ApplicationRecord.transaction do @filter.update!(keyword_params) diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index 54ff0e11d0..7c197ce6ba 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -53,15 +53,11 @@ class Api::V1::FollowRequestsController < Api::BaseController end def next_path - if records_continue? - api_v1_follow_requests_url pagination_params(max_id: pagination_max_id) - end + api_v1_follow_requests_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_follow_requests_url pagination_params(since_id: pagination_since_id) - end + api_v1_follow_requests_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index bad61425a5..3d55d990af 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -3,11 +3,12 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render_with_cache json: :activity, expires_in: 1.day end diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index 37a6906fb6..e954c45897 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -6,8 +6,15 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController before_action :require_enabled_api! before_action :set_domain_blocks + vary_by '', if: -> { Setting.show_domain_blocks == 'all' } + def index - expires_in 3.minutes, public: true + if Setting.show_domain_blocks == 'all' + cache_even_if_authenticated! + else + cache_if_unauthenticated! + end + render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?)) end diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb index c72e16cff2..a0665725bd 100644 --- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -2,11 +2,19 @@ class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale before_action :set_extended_description + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end + def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer end diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 2877fec52d..70281362a8 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -3,11 +3,18 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end def index - expires_in 1.day, public: true + cache_even_if_authenticated! render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) } end diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb index dbd69f54d4..36889f7335 100644 --- a/app/controllers/api/v1/instances/privacy_policies_controller.rb +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -5,8 +5,10 @@ class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController before_action :set_privacy_policy + vary_by '' + def show - expires_in 1.day, public: true + cache_even_if_authenticated! render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer end diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb index 93cf3c7594..d3eeca3262 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -2,10 +2,19 @@ class Api::V1::Instances::RulesController < Api::BaseController skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale before_action :set_rules + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end + def index + cache_even_if_authenticated! render json: @rules, each_serializer: REST::RuleSerializer end diff --git a/app/controllers/api/v1/instances/translation_languages_controller.rb b/app/controllers/api/v1/instances/translation_languages_controller.rb new file mode 100644 index 0000000000..c4680cccb8 --- /dev/null +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Api::V1::Instances::TranslationLanguagesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + + before_action :set_languages + + vary_by '' + + def show + cache_even_if_authenticated! + render json: @languages + end + + private + + def set_languages + if TranslationService.configured? + @languages = Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { TranslationService.configured.languages } + @languages['und'] = @languages.delete(nil) if @languages.key?(nil) + else + @languages = {} + end + end +end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 913319a869..5a6701ff96 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true class Api::V1::InstancesController < Api::BaseController - skip_before_action :set_cache_headers skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_around_action :set_locale + + vary_by '' + + # Override `current_user` to avoid reading session cookies unless in whitelist mode + def current_user + super if whitelist_mode? + end def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v1/lists/accounts_controller.rb b/app/controllers/api/v1/lists/accounts_controller.rb index b66ea9bfe6..8e12cb7b65 100644 --- a/app/controllers/api/v1/lists/accounts_controller.rb +++ b/app/controllers/api/v1/lists/accounts_controller.rb @@ -62,17 +62,13 @@ class Api::V1::Lists::AccountsController < Api::BaseController def next_path return if unlimited? - if records_continue? - api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) - end + api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path return if unlimited? - unless @accounts.empty? - api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) - end + api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 843ca2ec2b..4bbbed2673 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy) + params.permit(:title, :replies_policy, :exclusive) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index f9c935bf3e..5ea26d55bd 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -6,19 +6,20 @@ class Api::V1::MediaController < Api::BaseController before_action :set_media_attachment, except: [:create] before_action :check_processing, except: [:create] + def show + render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment + end + def create @media_attachment = current_account.media_attachments.create!(media_attachment_params) render json: @media_attachment, serializer: REST::MediaAttachmentSerializer rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 - rescue Paperclip::Error + rescue Paperclip::Error => e + Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end - def show - render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment - end - def update @media_attachment.update!(updateable_media_attachment_params) render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 6cde53a2a7..555485823c 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -33,15 +33,11 @@ class Api::V1::MutesController < Api::BaseController end def next_path - if records_continue? - api_v1_mutes_url pagination_params(max_id: pagination_max_id) - end + api_v1_mutes_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless paginated_mutes.empty? - api_v1_mutes_url pagination_params(since_id: pagination_since_id) - end + api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index a6ed359c98..7a64d13005 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -6,7 +6,7 @@ class Api::V1::NotificationsController < Api::BaseController before_action :require_user! after_action :insert_pagination_headers, only: :index - DEFAULT_NOTIFICATIONS_LIMIT = 15 + DEFAULT_NOTIFICATIONS_LIMIT = 40 def index @notifications = load_notifications @@ -28,7 +28,7 @@ class Api::V1::NotificationsController < Api::BaseController end def dismiss - current_account.notifications.find_by!(id: params[:id]).destroy! + current_account.notifications.find(params[:id]).destroy! render_empty end @@ -67,15 +67,11 @@ class Api::V1::NotificationsController < Api::BaseController end def next_path - unless @notifications.empty? - api_v1_notifications_url pagination_params(max_id: pagination_max_id) - end + api_v1_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty? end def prev_path - unless @notifications.empty? - api_v1_notifications_url pagination_params(min_id: pagination_since_id) - end + api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index 6435e9f0dc..ffc70a8496 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -8,6 +8,7 @@ class Api::V1::PollsController < Api::BaseController before_action :refresh_poll def show + cache_if_unauthenticated! render json: @poll, serializer: REST::PollSerializer, include_results: true end diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index 7148d63a4e..3634acf956 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -6,6 +6,10 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController before_action :set_push_subscription before_action :check_push_subscription, only: [:show, :update] + def show + render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer + end + def create @push_subscription&.destroy! @@ -21,10 +25,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer end - def show - render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer - end - def update @push_subscription.update!(data: data_params) render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb index f90642a738..2220b6d22e 100644 --- a/app/controllers/api/v1/scheduled_statuses_controller.rb +++ b/app/controllers/api/v1/scheduled_statuses_controller.rb @@ -52,15 +52,11 @@ class Api::V1::ScheduledStatusesController < Api::BaseController end def next_path - if records_continue? - api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id) - end + api_v1_scheduled_statuses_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @statuses.empty? - api_v1_scheduled_statuses_url pagination_params(min_id: pagination_since_id) - end + api_v1_scheduled_statuses_url pagination_params(min_id: pagination_since_id) unless @statuses.empty? end def records_continue? diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 2b614a8375..73eb11e711 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end @@ -41,15 +42,11 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController end def next_path - if records_continue? - api_v1_status_favourited_by_index_url pagination_params(max_id: pagination_max_id) - end + api_v1_status_favourited_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_status_favourited_by_index_url pagination_params(since_id: pagination_since_id) - end + api_v1_status_favourited_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index 7fe73a6f54..2913472b04 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -7,11 +7,16 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController before_action :set_status def show - render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer + cache_if_unauthenticated! + render json: status_edits, each_serializer: REST::StatusEditSerializer end private + def status_edits + @status.edits.includes(:account, status: [:account]).to_a.presence || [@status.build_snapshot(at_time: @status.edited_at || @status.created_at)] + end + def set_status @status = Status.find(params[:status_id]) authorize @status, :show? diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 24db30fcc0..41672e7539 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController after_action :insert_pagination_headers def index + cache_if_unauthenticated! @accounts = load_accounts render json: @accounts, each_serializer: REST::AccountSerializer end @@ -37,15 +38,11 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController end def next_path - if records_continue? - api_v1_status_reblogged_by_index_url pagination_params(max_id: pagination_max_id) - end + api_v1_status_reblogged_by_index_url pagination_params(max_id: pagination_max_id) if records_continue? end def prev_path - unless @accounts.empty? - api_v1_status_reblogged_by_index_url pagination_params(since_id: pagination_since_id) - end + api_v1_status_reblogged_by_index_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? end def pagination_max_id diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 1be15a5a43..e3769437b7 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -2,6 +2,8 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController include Authorization + include Redisable + include Lockable before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action :require_user! @@ -10,7 +12,9 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController override_rate_limit_headers :create, family: :statuses def create - @status = ReblogService.new.call(current_account, @reblog, reblog_params) + with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do + @status = ReblogService.new.call(current_account, @reblog, reblog_params) + end render json: @status, serializer: REST::StatusSerializer end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 3a9cf056ba..960f8cf765 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -24,11 +24,14 @@ class Api::V1::StatusesController < Api::BaseController DESCENDANTS_DEPTH_LIMIT = 20 def show + cache_if_unauthenticated! @status = cache_collection([@status], Status).first render json: @status, serializer: REST::StatusSerializer end def context + cache_if_unauthenticated! + ancestors_limit = CONTEXT_LIMIT descendants_limit = CONTEXT_LIMIT descendants_depth_limit = nil @@ -64,11 +67,18 @@ class Api::V1::StatusesController < Api::BaseController application: doorkeeper_token.application, poll: status_params[:poll], content_type: status_params[:content_type], + allowed_mentions: status_params[:allowed_mentions], idempotency: request.headers['Idempotency-Key'], with_rate_limit: true ) render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer + rescue PostStatusService::UnexpectedMentionsError => e + unexpected_accounts = ActiveModel::Serializer::CollectionSerializer.new( + e.accounts, + serializer: REST::AccountSerializer + ) + render json: { error: e.message, unexpected_accounts: unexpected_accounts }, status: 422 end def update @@ -131,6 +141,7 @@ class Api::V1::StatusesController < Api::BaseController :language, :scheduled_at, :content_type, + allowed_mentions: [], media_ids: [], media_attributes: [ :id, diff --git a/app/controllers/api/v1/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb index 7cd60615ab..0cdd00d62f 100644 --- a/app/controllers/api/v1/streaming_controller.rb +++ b/app/controllers/api/v1/streaming_controller.rb @@ -2,10 +2,10 @@ class Api::V1::StreamingController < Api::BaseController def index - if Rails.configuration.x.streaming_api_base_url != request.host - redirect_to streaming_api_url, status: 301 - else + if Rails.configuration.x.streaming_api_base_url == request.host not_found + else + redirect_to streaming_api_url, status: 301, allow_other_host: true end end diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb index 272362c314..284ec85937 100644 --- a/app/controllers/api/v1/tags_controller.rb +++ b/app/controllers/api/v1/tags_controller.rb @@ -8,6 +8,7 @@ class Api::V1::TagsController < Api::BaseController override_rate_limit_headers :follow, family: :follows def show + cache_if_unauthenticated! render json: @tag, serializer: REST::TagSerializer end @@ -25,6 +26,7 @@ class Api::V1::TagsController < Api::BaseController def set_or_create_tag return not_found unless Tag::HASHTAG_NAME_RE.match?(params[:id]) + @tag = Tag.find_normalized(params[:id]) || Tag.new(name: Tag.normalize(params[:id]), display_name: params[:id]) end end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 493fe4776a..6af504ff63 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end @@ -40,7 +41,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController only_media: truthy_param?(:only_media), allow_local_only: truthy_param?(:allow_local_only), with_replies: Setting.show_replies_in_public_timelines, - with_reblogs: Setting.show_reblogs_in_public_timelines, + with_reblogs: Setting.show_reblogs_in_public_timelines ) end diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 64a1db58df..9cd7b99046 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -5,6 +5,7 @@ class Api::V1::Timelines::TagController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } def show + cache_if_unauthenticated! @statuses = load_statuses render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb index 8ff3b364e2..57cfa0b7e4 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Api::V1::Trends::LinksController < Api::BaseController + vary_by 'Authorization, Accept-Language' + before_action :set_links after_action :insert_pagination_headers @@ -8,6 +10,7 @@ class Api::V1::Trends::LinksController < Api::BaseController DEFAULT_LINKS_LIMIT = 10 def index + cache_if_unauthenticated! render json: @links, each_serializer: REST::Trends::LinkSerializer end @@ -18,13 +21,11 @@ class Api::V1::Trends::LinksController < Api::BaseController end def set_links - @links = begin - if enabled? - links_from_trends.offset(offset_param).limit(limit_param(DEFAULT_LINKS_LIMIT)) - else - [] - end - end + @links = if enabled? + links_from_trends.offset(offset_param).limit(limit_param(DEFAULT_LINKS_LIMIT)) + else + [] + end end def links_from_trends diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb index c275d5fc81..c186864c3b 100644 --- a/app/controllers/api/v1/trends/statuses_controller.rb +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -1,11 +1,14 @@ # frozen_string_literal: true class Api::V1::Trends::StatusesController < Api::BaseController + vary_by 'Authorization, Accept-Language' + before_action :set_statuses after_action :insert_pagination_headers def index + cache_if_unauthenticated! render json: @statuses, each_serializer: REST::StatusSerializer end @@ -16,13 +19,11 @@ class Api::V1::Trends::StatusesController < Api::BaseController end def set_statuses - @statuses = begin - if enabled? - cache_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) - else - [] - end - end + @statuses = if enabled? + cache_collection(statuses_from_trends.offset(offset_param).limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status) + else + [] + end end def statuses_from_trends diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 885a4ad7e8..6cc8194def 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -8,6 +8,7 @@ class Api::V1::Trends::TagsController < Api::BaseController DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i def index + cache_if_unauthenticated! render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id) end @@ -18,13 +19,11 @@ class Api::V1::Trends::TagsController < Api::BaseController end def set_tags - @tags = begin - if enabled? - tags_from_trends.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT)) - else - [] - end - end + @tags = if enabled? + tags_from_trends.offset(offset_param).limit(limit_param(DEFAULT_TAGS_LIMIT)) + else + [] + end end def tags_from_trends diff --git a/app/controllers/api/v2/admin/accounts_controller.rb b/app/controllers/api/v2/admin/accounts_controller.rb index b25831aa09..65cf0c4db4 100644 --- a/app/controllers/api/v2/admin/accounts_controller.rb +++ b/app/controllers/api/v2/admin/accounts_controller.rb @@ -18,6 +18,14 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController private + def next_path + api_v2_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v2_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + def filtered_accounts AccountFilter.new(translated_filter_params).results end @@ -25,9 +33,7 @@ class Api::V2::Admin::AccountsController < Api::V1::Admin::AccountsController def translated_filter_params translated_params = filter_params.slice(*AccountFilter::KEYS) - if params[:permissions] == 'staff' - translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id) - end + translated_params[:role_ids] = UserRole.that_can(:manage_reports).map(&:id) if params[:permissions] == 'staff' translated_params end diff --git a/app/controllers/api/v2/filters/keywords_controller.rb b/app/controllers/api/v2/filters/keywords_controller.rb index c63e1d986b..fe1a991944 100644 --- a/app/controllers/api/v2/filters/keywords_controller.rb +++ b/app/controllers/api/v2/filters/keywords_controller.rb @@ -12,13 +12,13 @@ class Api::V2::Filters::KeywordsController < Api::BaseController render json: @keywords, each_serializer: REST::FilterKeywordSerializer end - def create - @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params) - + def show render json: @keyword, serializer: REST::FilterKeywordSerializer end - def show + def create + @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params) + render json: @keyword, serializer: REST::FilterKeywordSerializer end diff --git a/app/controllers/api/v2/filters/statuses_controller.rb b/app/controllers/api/v2/filters/statuses_controller.rb index 755c14cffa..2e95497a66 100644 --- a/app/controllers/api/v2/filters/statuses_controller.rb +++ b/app/controllers/api/v2/filters/statuses_controller.rb @@ -12,13 +12,13 @@ class Api::V2::Filters::StatusesController < Api::BaseController render json: @status_filters, each_serializer: REST::FilterStatusSerializer end - def create - @status_filter = current_account.custom_filters.find(params[:filter_id]).statuses.create!(resource_params) - + def show render json: @status_filter, serializer: REST::FilterStatusSerializer end - def show + def create + @status_filter = current_account.custom_filters.find(params[:filter_id]).statuses.create!(resource_params) + render json: @status_filter, serializer: REST::FilterStatusSerializer end diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 8ff3076cfb..2fcdeeae45 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -11,13 +11,13 @@ class Api::V2::FiltersController < Api::BaseController render json: @filters, each_serializer: REST::FilterSerializer, rules_requested: true end - def create - @filter = current_account.custom_filters.create!(resource_params) - + def show render json: @filter, serializer: REST::FilterSerializer, rules_requested: true end - def show + def create + @filter = current_account.custom_filters.create!(resource_params) + render json: @filter, serializer: REST::FilterSerializer, rules_requested: true end diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index bcd90cff22..8346e28830 100644 --- a/app/controllers/api/v2/instances_controller.rb +++ b/app/controllers/api/v2/instances_controller.rb @@ -2,7 +2,7 @@ class Api::V2::InstancesController < Api::V1::InstancesController def show - expires_in 3.minutes, public: true + cache_even_if_authenticated! render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v2/media_controller.rb b/app/controllers/api/v2/media_controller.rb index 288f847f17..72bc694421 100644 --- a/app/controllers/api/v2/media_controller.rb +++ b/app/controllers/api/v2/media_controller.rb @@ -6,7 +6,8 @@ class Api::V2::MediaController < Api::V1::MediaController render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: @media_attachment.not_processed? ? 202 : 200 rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 - rescue Paperclip::Error + rescue Paperclip::Error => e + Rails.logger.error "#{e.class}: #{e.message}" render json: processing_error, status: 500 end end diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index b084eae425..cc74db58e5 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -34,11 +34,11 @@ class Api::V2::SearchController < Api::BaseController params[:q], current_account, limit_param(RESULTS_LIMIT), - search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed)) + search_params.merge(resolve: truthy_param?(:resolve), exclude_unreviewed: truthy_param?(:exclude_unreviewed), following: truthy_param?(:following)) ) end def search_params - params.permit(:type, :offset, :min_id, :max_id, :account_id) + params.permit(:type, :offset, :min_id, :max_id, :account_id, :following) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ee3c5204d8..7c09040fbf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -18,7 +18,11 @@ class ApplicationController < ActionController::Base helper_method :current_skin helper_method :single_user_mode? helper_method :use_seamless_external_login? + helper_method :omniauth_only? + helper_method :sso_account_settings helper_method :whitelist_mode? + helper_method :body_class_string + helper_method :skip_csrf_meta_tags? rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request rescue_from Mastodon::NotPermittedError, with: :forbidden @@ -35,9 +39,11 @@ class ApplicationController < ActionController::Base service_unavailable end - before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? + before_action :store_referrer, except: :raise_not_found, if: :devise_controller? before_action :require_functional!, if: :user_signed_in? + before_action :set_cache_control_defaults + skip_before_action :verify_authenticity_token, only: :raise_not_found def raise_not_found @@ -54,16 +60,31 @@ class ApplicationController < ActionController::Base !authorized_fetch_mode? end - def store_current_location - store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym) + def store_referrer + return if request.referer.blank? + + redirect_uri = URI(request.referer) + return if redirect_uri.path.start_with?('/auth') + + stored_url = redirect_uri.to_s if redirect_uri.host == request.host && redirect_uri.port == request.port + + store_location_for(:user, stored_url) end def require_functional! redirect_to edit_user_registration_path unless current_user.functional? end + def skip_csrf_meta_tags? + false + end + def after_sign_out_path_for(_resource_or_scope) - new_user_session_path + if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true' + '/auth/auth/openid_connect/logout' + else + new_user_session_path + end end protected @@ -116,6 +137,14 @@ class ApplicationController < ActionController::Base Devise.pam_authentication || Devise.ldap_authentication end + def omniauth_only? + ENV['OMNIAUTH_ONLY'] == 'true' + end + + def sso_account_settings + ENV.fetch('SSO_ACCOUNT_SETTINGS', nil) + end + def current_account return @current_account if defined?(@current_account) @@ -128,6 +157,10 @@ class ApplicationController < ActionController::Base @current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present? end + def body_class_string + @body_classes || '' + end + def respond_with_error(code) respond_to do |format| format.any do @@ -137,4 +170,8 @@ class ApplicationController < ActionController::Base format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } end end + + def set_cache_control_defaults + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 0817a905ca..3283c5f362 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -15,12 +15,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController skip_before_action :require_functional! - def new - super - - resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? - end - def show old_session_values = session.to_hash reset_session @@ -29,6 +23,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController super end + def new + super + + resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? + end + def confirm_captcha check_captcha! do |message| flash.now[:alert] = message @@ -51,14 +51,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController # step. confirmation_token = params[:confirmation_token] return if confirmation_token.nil? + @confirmation_user = User.find_first_by_auth_conditions(confirmation_token: confirmation_token) end def captcha_user_bypass? return true if @confirmation_user.nil? || @confirmation_user.confirmed? - - invite = Invite.find(@confirmation_user.invite_id) if @confirmation_user.invite_id.present? - invite.present? && !invite.max_uses.nil? end def set_pack @@ -90,8 +88,10 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController def after_confirmation_path_for(_resource_name, user) if user.created_by_application && truthy_param?(:redirect_to_app) user.created_by_application.confirmation_redirect_uri + elsif user_signed_in? + web_url('start') else - super + new_user_session_path end end end diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 3d7962de56..9e0fb942aa 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -33,7 +33,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController def after_sign_in_path_for(resource) if resource.email_present? - root_path + stored_location_for(resource) || root_path else auth_setup_path(missing_email: '1') end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 40c38bc6dd..a9d92b6e2b 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -25,18 +25,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(&:build_invite_request) end - def destroy - not_found - end - def update super do |resource| - if resource.saved_change_to_encrypted_password? - resource.clear_other_sessions(current_session.session_id) - end + resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password? end end + def destroy + not_found + end + protected def update_resource(resource, params) @@ -49,7 +47,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(hash) resource.locale = I18n.locale - resource.invite_code = params[:invite_code] if resource.invite_code.blank? + resource.invite_code = @invite&.code if resource.invite_code.blank? resource.registration_form_time = session[:registration_form_time] resource.sign_up_ip = request.remote_ip @@ -134,7 +132,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_sessions - @sessions = current_user.session_activations + @sessions = current_user.session_activations.order(updated_at: :desc) end def set_strikes @@ -159,6 +157,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 16c18baa24..b1abb9f1df 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -53,9 +53,9 @@ class Auth::SessionsController < Devise::SessionsController session[:webauthn_challenge] = options_for_get.challenge - render json: options_for_get, status: :ok + render json: options_for_get, status: 200 else - render json: { error: t('webauthn_credentials.not_enabled') }, status: :unauthorized + render json: { error: t('webauthn_credentials.not_enabled') }, status: 401 end end @@ -115,9 +115,7 @@ class Auth::SessionsController < Devise::SessionsController def home_paths(resource) paths = [about_path] - if single_user_mode? && resource.is_a?(User) - paths << short_account_path(username: resource.account) - end + paths << short_account_path(username: resource.account) if single_user_mode? && resource.is_a?(User) paths end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index db5a866f21..8edca4d01b 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -11,15 +11,7 @@ class Auth::SetupController < ApplicationController skip_before_action :require_functional! - def show - flash.now[:notice] = begin - if @user.pending? - I18n.t('devise.registrations.signed_up_but_pending') - else - I18n.t('devise.registrations.signed_up_but_unconfirmed') - end - end - end + def show; end def update # This allows updating the e-mail without entering a password as is required @@ -27,14 +19,13 @@ class Auth::SetupController < ApplicationController # that were not confirmed yet if @user.update(user_params) - redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions') + @user.resend_confirmation_instructions unless @user.confirmed? + redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent') else render :show end end - helper_method :missing_email? - private def require_unconfirmed_or_pending! @@ -53,11 +44,7 @@ class Auth::SetupController < ApplicationController params.require(:user).permit(:email) end - def missing_email? - truthy_param?(:missing_email) - end - def set_pack - use_pack 'auth' + use_pack 'sign_up' end end diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index 97fe4a9abd..73f0f2b88d 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -60,7 +60,7 @@ class AuthorizeInteractionsController < ApplicationController end def uri_param - params[:uri] || params.fetch(:acct, '').gsub(/\Aacct:/, '') + params[:uri] || params.fetch(:acct, '').delete_prefix('acct:') end def set_body_classes diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb new file mode 100644 index 0000000000..205df48d44 --- /dev/null +++ b/app/controllers/backups_controller.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class BackupsController < ApplicationController + include RoutingHelper + + skip_before_action :require_functional! + + before_action :authenticate_user! + before_action :set_backup + + def download + case Paperclip::Attachment.default_options[:storage] + when :s3 + redirect_to @backup.dump.expiring_url(10), allow_other_host: true + when :fog + if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? + redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true + else + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true + end + when :filesystem + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true + end + end + + private + + def set_backup + @backup = current_user.backups.find(params[:id]) + end +end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index 2f7d84df04..e9cff22ca8 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -10,7 +10,8 @@ module AccountControllerConcern included do before_action :set_instance_presenter - before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } + + after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } end private diff --git a/app/controllers/concerns/api_caching_concern.rb b/app/controllers/concerns/api_caching_concern.rb new file mode 100644 index 0000000000..705abce80f --- /dev/null +++ b/app/controllers/concerns/api_caching_concern.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ApiCachingConcern + extend ActiveSupport::Concern + + def cache_if_unauthenticated! + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + end + + def cache_even_if_authenticated! + expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless whitelist_mode? + end +end diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 05e431b19a..55ebe1bd64 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -3,8 +3,182 @@ module CacheConcern extend ActiveSupport::Concern + module ActiveRecordCoder + EMPTY_HASH = {}.freeze + + class << self + def dump(record) + instances = InstanceTracker.new + serialized_associations = serialize_associations(record, instances) + serialized_records = instances.map { |r| serialize_record(r) } + [serialized_associations, *serialized_records] + end + + def load(payload) + instances = InstanceTracker.new + serialized_associations, *serialized_records = payload + serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) } + deserialize_associations(serialized_associations, instances) + end + + private + + # Records without associations, or which have already been visited before, + # are serialized by their id alone. + # + # Records with associations are serialized as a two-element array including + # their id and the record's association cache. + # + def serialize_associations(record, instances) + return unless record + + if (id = instances.lookup(record)) + payload = id + else + payload = instances.push(record) + + cached_associations = record.class.reflect_on_all_associations.select do |reflection| + record.association_cached?(reflection.name) + end + + unless cached_associations.empty? + serialized_associations = cached_associations.map do |reflection| + association = record.association(reflection.name) + + serialized_target = if reflection.collection? + association.target.map { |target_record| serialize_associations(target_record, instances) } + else + serialize_associations(association.target, instances) + end + + [reflection.name, serialized_target] + end + + payload = [payload, serialized_associations] + end + end + + payload + end + + def deserialize_associations(payload, instances) + return unless payload + + id, associations = payload + record = instances.fetch(id) + + associations&.each do |name, serialized_target| + begin + association = record.association(name) + rescue ActiveRecord::AssociationNotFoundError + raise AssociationMissingError, "undefined association: #{name}" + end + + target = if association.reflection.collection? + serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) } + else + deserialize_associations(serialized_target, instances) + end + + association.target = target + end + + record + end + + def serialize_record(record) + arguments = [record.class.name, attributes_for_database(record)] + arguments << true if record.new_record? + arguments + end + + if Rails.gem_version >= Gem::Version.new('7.0') + def attributes_for_database(record) + attributes = record.attributes_for_database + attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } + attributes + end + else + def attributes_for_database(record) + attributes = record.instance_variable_get(:@attributes).send(:attributes).transform_values(&:value_for_database) + attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr } + attributes + end + end + + def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter + begin + klass = Object.const_get(class_name) + rescue NameError + raise ClassMissingError, "undefined class: #{class_name}" + end + + # Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass + # wether the record was persisted or not. + attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH) + klass.allocate.init_with_attributes(attributes, new_record) + end + end + + class Error < StandardError + end + + class ClassMissingError < Error + end + + class AssociationMissingError < Error + end + + class InstanceTracker + def initialize + @instances = [] + @ids = {}.compare_by_identity + end + + def map(&block) + @instances.map(&block) + end + + def fetch(...) + @instances.fetch(...) + end + + def push(instance) + id = @ids[instance] = @instances.size + @instances << instance + id + end + + def lookup(instance) + @ids[instance] + end + end + end + + class_methods do + def vary_by(value, **kwargs) + before_action(**kwargs) do |controller| + response.headers['Vary'] = value.respond_to?(:call) ? controller.instance_exec(&value) : value + end + end + end + + included do + after_action :enforce_cache_control! + end + + # Prevents high-entropy headers such as `Cookie`, `Signature` or `Authorization` + # from being used as cache keys, while allowing to `Vary` on them (to not serve + # anonymous cached data to authenticated requests when authentication matters) + def enforce_cache_control! + vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase } + return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? } + + response.cache_control.replace(private: true, no_store: true) + end + def render_with_cache(**options) - raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given? + raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given? key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':') expires_in = options.delete(:expires_in) || 3.minutes @@ -24,18 +198,19 @@ module CacheConcern end end - def set_cache_headers - response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature' - end - def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation) return [] if raw.empty? - cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id) - uncached_ids = raw.map(&:id) - cached_keys_with_value.keys + cached_keys_with_value = begin + Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) } + rescue ActiveRecordCoder::Error + {} # The serialization format may have changed, let's pretend it's a cache miss. + end + + uncached_ids = raw.map(&:id) - cached_keys_with_value.keys klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!) @@ -43,7 +218,7 @@ module CacheConcern uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) uncached.each_value do |item| - Rails.cache.write(item, item) + Rails.cache.write(item, ActiveRecordCoder.dump(item)) end end diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb index 538c1ffb14..576304d1ca 100644 --- a/app/controllers/concerns/captcha_concern.rb +++ b/app/controllers/concerns/captcha_concern.rb @@ -2,6 +2,7 @@ module CaptchaConcern extend ActiveSupport::Concern + include Hcaptcha::Adapters::ViewMethods included do @@ -35,18 +36,22 @@ module CaptchaConcern flash.delete(:hcaptcha_error) yield message end + false end end def extend_csp_for_captcha! policy = request.content_security_policy + return unless captcha_required? && policy.present? %w(script_src frame_src style_src connect_src).each do |directive| values = policy.send(directive) + values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:') values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:') + policy.send(directive, *values) end end diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb index b8696df736..30702f00e7 100644 --- a/app/controllers/concerns/rate_limit_headers.rb +++ b/app/controllers/concerns/rate_limit_headers.rb @@ -6,13 +6,11 @@ module RateLimitHeaders class_methods do def override_rate_limit_headers(method_name, options = {}) around_action(only: method_name, if: :current_account) do |_controller, block| - begin - block.call - ensure - rate_limiter = RateLimiter.new(current_account, options) - rate_limit_headers = rate_limiter.to_headers - response.headers.merge!(rate_limit_headers) unless response.headers['X-RateLimit-Remaining'].present? && rate_limit_headers['X-RateLimit-Remaining'].to_i > response.headers['X-RateLimit-Remaining'].to_i - end + block.call + ensure + rate_limiter = RateLimiter.new(current_account, options) + rate_limit_headers = rate_limiter.to_headers + response.headers.merge!(rate_limit_headers) unless response.headers['X-RateLimit-Remaining'].present? && rate_limit_headers['X-RateLimit-Remaining'].to_i > response.headers['X-RateLimit-Remaining'].to_i end end end @@ -67,6 +65,6 @@ module RateLimitHeaders end def reset_period_offset - api_throttle_data[:period] - request_time.to_i % api_throttle_data[:period] + api_throttle_data[:period] - (request_time.to_i % api_throttle_data[:period]) end end diff --git a/app/controllers/concerns/session_tracking_concern.rb b/app/controllers/concerns/session_tracking_concern.rb index eaaa4ac597..3f56c0d026 100644 --- a/app/controllers/concerns/session_tracking_concern.rb +++ b/app/controllers/concerns/session_tracking_concern.rb @@ -13,6 +13,7 @@ module SessionTrackingConcern def set_session_activity return unless session_needs_update? + current_session.touch end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index a9950d21fb..1d27c92c8c 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -138,7 +138,7 @@ module SignatureVerification end def signed_headers - signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split(' ') + signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split end def verify_signature_strength! @@ -165,6 +165,7 @@ module SignatureVerification end raise SignatureVerificationError, "Invalid Digest value. The provided Digest value is not a SHA-256 digest. Given digest: #{sha256[1]}" if digest_size != 32 + raise SignatureVerificationError, "Invalid Digest value. Computed SHA-256 digest: #{body_digest}; given: #{sha256[1]}" end @@ -179,14 +180,15 @@ module SignatureVerification def build_signed_string signed_headers.map do |signed_header| - if signed_header == Request::REQUEST_TARGET + case signed_header + when Request::REQUEST_TARGET "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" - elsif signed_header == '(created)' + when '(created)' raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank? "(created): #{signature_params['created']}" - elsif signed_header == '(expires)' + when '(expires)' raise SignatureVerificationError, 'Invalid pseudo-header (expires) for rsa-sha256' unless signature_algorithm == 'hs2019' raise SignatureVerificationError, 'Pseudo-header (expires) used but corresponding argument missing' if signature_params['expires'].blank? @@ -227,7 +229,7 @@ module SignatureVerification end def to_header_name(name) - name.split(/-/).map(&:capitalize).join('-') + name.split('-').map(&:capitalize).join('-') end def missing_required_signature_parameters? @@ -243,7 +245,7 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), suppress_errors: false) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } elsif !ActivityPub::TagManager.instance.local_uri?(key_id) account = ActivityPub::TagManager.instance.uri_to_actor(key_id) account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) } diff --git a/app/controllers/concerns/theming_concern.rb b/app/controllers/concerns/theming_concern.rb index f993a81d72..82a53dbf51 100644 --- a/app/controllers/concerns/theming_concern.rb +++ b/app/controllers/concerns/theming_concern.rb @@ -75,7 +75,7 @@ module ThemingConcern end fallbacks.each do |fallback| - return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback) + return resolve_pack(Themes.instance.flavour(fallback), pack_name, skin) if Themes.instance.flavour(fallback) end nil diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb index c9477a1d42..b30cd354d2 100644 --- a/app/controllers/concerns/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -57,10 +57,10 @@ module TwoFactorAuthenticationConcern if valid_webauthn_credential?(user, webauthn_credential) on_authentication_success(user, :webauthn) - render json: { redirect_path: after_sign_in_path_for(user) }, status: :ok + render json: { redirect_path: after_sign_in_path_for(user) }, status: 200 else on_authentication_failure(user, :webauthn, :invalid_credential) - render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity + render json: { error: t('webauthn_credentials.invalid_credential') }, status: 422 end end @@ -81,13 +81,11 @@ module TwoFactorAuthenticationConcern @body_classes = 'lighter' @webauthn_enabled = user.webauthn_enabled? - @scheme_type = begin - if user.webauthn_enabled? && user_params[:otp_attempt].blank? - 'webauthn' - else - 'totp' - end - end + @scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank? + 'webauthn' + else + 'totp' + end set_locale { render :two_factor } end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 7ba7a57e3d..96c31566e0 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -7,6 +7,12 @@ module WebAppControllerConcern prepend_before_action :redirect_unauthenticated_to_permalinks! before_action :set_pack before_action :set_app_body_class + + vary_by 'Accept, Accept-Language, Cookie' + end + + def skip_csrf_meta_tags? + current_user.nil? end def set_app_body_class diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 9270c467dc..e7a02ea89c 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,18 +1,8 @@ # frozen_string_literal: true -class CustomCssController < ApplicationController - skip_before_action :store_current_location - skip_before_action :require_functional! - skip_before_action :update_user_sign_in - skip_before_action :set_session_activity - - skip_around_action :set_locale - - before_action :set_cache_headers - +class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController def show expires_in 3.minutes, public: true - request.session_options[:skip] = true render content_type: 'text/css' end end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index 7830c55247..f51f44c620 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -10,6 +10,7 @@ class Disputes::BaseController < ApplicationController before_action :set_body_classes before_action :authenticate_user! before_action :set_pack + before_action :set_cache_headers private @@ -20,4 +21,8 @@ class Disputes::BaseController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/emojis_controller.rb b/app/controllers/emojis_controller.rb index 41f1e1c5ca..72bc56de04 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -2,15 +2,12 @@ class EmojisController < ApplicationController before_action :set_emoji - before_action :set_cache_headers + + vary_by -> { 'Signature' if authorized_fetch_mode? } def show - respond_to do |format| - format.json do - expires_in 3.minutes, public: true - render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter - end - end + expires_in 3.minutes, public: true + render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter end private diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index 4f63de7b69..97206c7eda 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -8,6 +8,7 @@ class Filters::StatusesController < ApplicationController before_action :set_status_filters before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers PER_PAGE = 20 @@ -43,12 +44,14 @@ class Filters::StatusesController < ApplicationController end def action_from_button - if params[:remove] - 'remove' - end + 'remove' if params[:remove] end def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index 2ab3b0a744..180ddf070b 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -7,6 +7,7 @@ class FiltersController < ApplicationController before_action :set_filter, only: [:edit, :update, :destroy] before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers def index @filters = current_account.custom_filters.includes(:keywords, :statuses).order(:phrase) @@ -17,6 +18,8 @@ class FiltersController < ApplicationController @filter.keywords.build end + def edit; end + def create @filter = current_account.custom_filters.build(resource_params) @@ -27,8 +30,6 @@ class FiltersController < ApplicationController end end - def edit; end - def update if @filter.update(resource_params) redirect_to filters_path @@ -59,4 +60,8 @@ class FiltersController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 1f5ed30de9..2e55cf6c34 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -5,8 +5,9 @@ class FollowerAccountsController < ApplicationController include SignatureVerification include WebAppControllerConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -14,7 +15,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.json do diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index febd13c975..2aa31bdf08 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -5,8 +5,9 @@ class FollowingAccountsController < ApplicationController include SignatureVerification include WebAppControllerConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :set_cache_headers skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, unless: :whitelist_mode? @@ -14,7 +15,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.json do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index d8ee82a7a2..ee940e6707 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -6,7 +6,7 @@ class HomeController < ApplicationController before_action :set_instance_presenter def index - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 0853897f20..8422d74bc3 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true -class InstanceActorsController < ApplicationController - include AccountControllerConcern +class InstanceActorsController < ActivityPub::BaseController + vary_by '' - skip_before_action :check_account_confirmation - skip_around_action :set_locale + serialization_scope nil + + before_action :set_account + skip_before_action :require_functional! + skip_before_action :update_user_sign_in def show expires_in 10.minutes, public: true diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index ca89fc7fe6..ea024e30e6 100644 --- a/app/controllers/intents_controller.rb +++ b/app/controllers/intents_controller.rb @@ -9,7 +9,7 @@ class IntentsController < ApplicationController if uri.scheme == 'web+mastodon' case uri.host when 'follow' - return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, '')) + return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].delete_prefix('acct:')) when 'share' return redirect_to share_path(text: uri.query_values['text']) end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 0b3c082dce..2db4bc5cbd 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -8,6 +8,7 @@ class InvitesController < ApplicationController before_action :authenticate_user! before_action :set_pack before_action :set_body_classes + before_action :set_cache_headers def index authorize :invite, :create? @@ -54,4 +55,8 @@ class InvitesController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/mail_subscriptions_controller.rb b/app/controllers/mail_subscriptions_controller.rb new file mode 100644 index 0000000000..b071a80605 --- /dev/null +++ b/app/controllers/mail_subscriptions_controller.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class MailSubscriptionsController < ApplicationController + layout 'auth' + + skip_before_action :require_functional! + + before_action :set_body_classes + before_action :set_user + before_action :set_type + + def show; end + + def create + @user.settings[email_type_from_param] = false + @user.save! + end + + private + + def set_user + @user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe') + end + + def set_body_classes + @body_classes = 'lighter' + end + + def set_type + @type = email_type_from_param + end + + def email_type_from_param + case params[:type] + when 'follow', 'reblog', 'favourite', 'mention', 'follow_request' + "notification_emails.#{params[:type]}" + else + raise ArgumentError + end + end +end diff --git a/app/controllers/manifests_controller.rb b/app/controllers/manifests_controller.rb index 960510f601..4fba9198f3 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -class ManifestsController < ApplicationController - skip_before_action :store_current_location - skip_before_action :require_functional! +class ManifestsController < ActionController::Base # rubocop:disable Rails/ApplicationController + # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` + # and thus re-issuing session cookies + serialization_scope nil def show expires_in 3.minutes, public: true diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index f9160d8c43..ac820e92bc 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -3,7 +3,6 @@ class MediaController < ApplicationController include Authorization - skip_before_action :store_current_location skip_before_action :require_functional!, unless: :whitelist_mode? before_action :authenticate_user!, if: :whitelist_mode? @@ -33,7 +32,7 @@ class MediaController < ApplicationController scope = MediaAttachment.local.attached # If id is 19 characters long, it's a shortcode, otherwise it's an identifier - @media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find_by!(id: id) + @media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find(id) end def verify_permitted_status! @@ -47,7 +46,7 @@ class MediaController < ApplicationController end def allow_iframing - response.headers['X-Frame-Options'] = 'ALLOWALL' + response.headers.delete('X-Frame-Options') end def set_pack diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 3b228722f3..8d480d704e 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -6,7 +6,6 @@ class MediaProxyController < ApplicationController include Redisable include Lockable - skip_before_action :store_current_location skip_before_action :require_functional! before_action :authenticate_user!, if: :whitelist_mode? @@ -17,13 +16,13 @@ class MediaProxyController < ApplicationController rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show - with_lock("media_download:#{params[:id]}") do + with_redis_lock("media_download:#{params[:id]}") do @media_attachment = MediaAttachment.remote.attached.find(params[:id]) authorize @media_attachment.status, :show? redownload! if @media_attachment.needs_redownload? && !reject_media? end - redirect_to full_asset_url(@media_attachment.file.url(version)) + redirect_to full_asset_url(@media_attachment.file.url(version)), allow_other_host: true end private diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index d6e7d0800c..62fc9c1b0d 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -39,6 +39,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index b2564a7915..0a1df55066 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -8,6 +8,9 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :set_pack before_action :require_not_suspended!, only: :destroy before_action :set_body_classes + before_action :set_cache_headers + + before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json } skip_before_action :require_functional! @@ -35,4 +38,18 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def require_not_suspended! forbidden if current_account.suspended? end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end + + def set_last_used_at_by_app + @last_used_at_by_app = Doorkeeper::AccessToken + .select('DISTINCT ON (application_id) application_id, last_used_at') + .where(resource_owner_id: current_resource_owner.id) + .where.not(last_used_at: nil) + .order(application_id: :desc, last_used_at: :desc) + .pluck(:application_id, :last_used_at) + .to_h + end end diff --git a/app/controllers/privacy_controller.rb b/app/controllers/privacy_controller.rb index 2c98bf3bf4..070ee8a06a 100644 --- a/app/controllers/privacy_controller.rb +++ b/app/controllers/privacy_controller.rb @@ -8,7 +8,7 @@ class PrivacyController < ApplicationController before_action :set_instance_presenter def show - expires_in 0, public: true if current_account.nil? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? end private diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index d40770726c..f83098f731 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -8,6 +8,7 @@ class RelationshipsController < ApplicationController before_action :set_pack before_action :set_relationships, only: :show before_action :set_body_classes + before_action :set_cache_headers helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship? @@ -20,6 +21,8 @@ class RelationshipsController < ApplicationController @form.save rescue ActionController::ParameterMissing # Do nothing + rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound + flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow' ensure redirect_to relationships_path(filter_params) end @@ -61,8 +64,8 @@ class RelationshipsController < ApplicationController 'unfollow' elsif params[:remove_from_followers] 'remove_from_followers' - elsif params[:block_domains] - 'block_domains' + elsif params[:block_domains] || params[:remove_domains_from_followers] + 'remove_domains_from_followers' end end @@ -73,4 +76,8 @@ class RelationshipsController < ApplicationController def set_pack use_pack 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index d3ac268d86..d4b7205681 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -8,6 +8,8 @@ class Settings::ApplicationsController < Settings::BaseController @applications = current_user.applications.order(id: :desc).page(params[:page]) end + def show; end + def new @application = Doorkeeper::Application.new( redirect_uri: Doorkeeper.configuration.native_redirect_uri, @@ -15,8 +17,6 @@ class Settings::ApplicationsController < Settings::BaseController ) end - def show; end - def create @application = current_user.applications.build(application_params) @@ -29,7 +29,13 @@ class Settings::ApplicationsController < Settings::BaseController def update if @application.update(application_params) - redirect_to settings_applications_path, notice: I18n.t('generic.changes_saved_msg') + if @application.scopes_previously_changed? + @access_token = current_user.token_for_app(@application) + @access_token.destroy + redirect_to settings_application_path(@application), notice: I18n.t('applications.token_regenerated') + else + redirect_to settings_application_path(@application), notice: I18n.t('generic.changes_saved_msg') + end else render :show end diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index bf17b918cc..56aeb49aa0 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -19,7 +19,7 @@ class Settings::BaseController < ApplicationController end def set_cache_headers - response.headers['Cache-Control'] = 'private, no-store' + response.cache_control.replace(private: true, no_store: true) end def require_not_suspended! diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index deaa7940eb..46a340aeb3 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -15,7 +15,7 @@ class Settings::ExportsController < Settings::BaseController def create backup = nil - with_lock("backup:#{current_user.id}") do + with_redis_lock("backup:#{current_user.id}") do authorize :backup, :create? backup = current_user.backups.create! end diff --git a/app/controllers/settings/flavours_controller.rb b/app/controllers/settings/flavours_controller.rb index 62c52eee97..b179b9429f 100644 --- a/app/controllers/settings/flavours_controller.rb +++ b/app/controllers/settings/flavours_controller.rb @@ -12,27 +12,15 @@ class Settings::FlavoursController < Settings::BaseController end def show - unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) - redirect_to action: 'show', flavour: current_flavour - end + redirect_to action: 'show', flavour: current_flavour unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) @listing = Themes.instance.flavours @selected = params[:flavour] end def update - user_settings.update(user_settings_params) + current_user.settings.update(flavour: params.require(:flavour), skin: params.dig(:user, :setting_skin)) + current_user.save redirect_to action: 'show', flavour: params[:flavour] end - - private - - def user_settings - UserSettingsDecorator.new(current_user) - end - - def user_settings_params - { setting_flavour: params.require(:flavour), - setting_skin: params.dig(:user, :setting_skin) }.with_indifferent_access - end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index d4516526ee..983caf22fa 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,31 +1,101 @@ # frozen_string_literal: true -class Settings::ImportsController < Settings::BaseController - before_action :set_account +require 'csv' - def show - @import = Import.new +class Settings::ImportsController < Settings::BaseController + before_action :set_bulk_import, only: [:show, :confirm, :destroy] + before_action :set_recent_imports, only: [:index] + + TYPE_TO_FILENAME_MAP = { + following: 'following_accounts_failures.csv', + blocking: 'blocked_accounts_failures.csv', + muting: 'muted_accounts_failures.csv', + domain_blocking: 'blocked_domains_failures.csv', + bookmarks: 'bookmarks_failures.csv', + lists: 'lists_failures.csv', + }.freeze + + TYPE_TO_HEADERS_MAP = { + following: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], + blocking: false, + muting: ['Account address', 'Hide notifications'], + domain_blocking: false, + bookmarks: false, + lists: false, + }.freeze + + def index + @import = Form::Import.new(current_account: current_account) + end + + def show; end + + def failures + @bulk_import = current_account.bulk_imports.where(state: :finished).find(params[:id]) + + respond_to do |format| + format.csv do + filename = TYPE_TO_FILENAME_MAP[@bulk_import.type.to_sym] + headers = TYPE_TO_HEADERS_MAP[@bulk_import.type.to_sym] + + export_data = CSV.generate(headers: headers, write_headers: true) do |csv| + @bulk_import.rows.find_each do |row| + case @bulk_import.type.to_sym + when :following + csv << [row.data['acct'], row.data.fetch('show_reblogs', true), row.data.fetch('notify', false), row.data['languages']&.join(', ')] + when :blocking + csv << [row.data['acct']] + when :muting + csv << [row.data['acct'], row.data.fetch('hide_notifications', true)] + when :domain_blocking + csv << [row.data['domain']] + when :bookmarks + csv << [row.data['uri']] + when :lists + csv << [row.data['list_name'], row.data['acct']] + end + end + end + + send_data export_data, filename: filename + end + end + end + + def confirm + @bulk_import.update!(state: :scheduled) + BulkImportWorker.perform_async(@bulk_import.id) + redirect_to settings_imports_path, notice: I18n.t('imports.success') end def create - @import = Import.new(import_params) - @import.account = @account + @import = Form::Import.new(import_params.merge(current_account: current_account)) if @import.save - ImportWorker.perform_async(@import.id) - redirect_to settings_import_path, notice: I18n.t('imports.success') + redirect_to settings_import_path(@import.bulk_import.id) else - render :show + # We need to set recent imports as we are displaying the index again + set_recent_imports + render :index end end + def destroy + @bulk_import.destroy! + redirect_to settings_imports_path + end + private - def set_account - @account = current_user.account + def import_params + params.require(:form_import).permit(:data, :type, :mode) end - def import_params - params.require(:import).permit(:data, :type, :mode) + def set_bulk_import + @bulk_import = current_account.bulk_imports.where(state: :unconfirmed).find(params[:id]) + end + + def set_recent_imports + @recent_imports = current_account.bulk_imports.reorder(id: :desc).limit(10) end end diff --git a/app/controllers/settings/preferences/appearance_controller.rb b/app/controllers/settings/preferences/appearance_controller.rb index 80ea57bd2d..4d7d12bb7f 100644 --- a/app/controllers/settings/preferences/appearance_controller.rb +++ b/app/controllers/settings/preferences/appearance_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::AppearanceController < Settings::PreferencesController +class Settings::Preferences::AppearanceController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb new file mode 100644 index 0000000000..c1f8b49898 --- /dev/null +++ b/app/controllers/settings/preferences/base_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Settings::Preferences::BaseController < Settings::BaseController + def show; end + + def update + if current_user.update(user_params) + I18n.locale = current_user.locale + redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') + else + render :show + end + end + + private + + def after_update_redirect_path + raise 'Override in controller' + end + + def user_params + params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys) + end +end diff --git a/app/controllers/settings/preferences/notifications_controller.rb b/app/controllers/settings/preferences/notifications_controller.rb index a16ae6a672..66d6c9a2f7 100644 --- a/app/controllers/settings/preferences/notifications_controller.rb +++ b/app/controllers/settings/preferences/notifications_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::NotificationsController < Settings::PreferencesController +class Settings::Preferences::NotificationsController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences/other_controller.rb b/app/controllers/settings/preferences/other_controller.rb index 07eb89a762..a19fbf5c48 100644 --- a/app/controllers/settings/preferences/other_controller.rb +++ b/app/controllers/settings/preferences/other_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Settings::Preferences::OtherController < Settings::PreferencesController +class Settings::Preferences::OtherController < Settings::Preferences::BaseController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb deleted file mode 100644 index 4c13364369..0000000000 --- a/app/controllers/settings/preferences_controller.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -class Settings::PreferencesController < Settings::BaseController - def show; end - - def update - user_settings.update(user_settings_params.to_h) - - if current_user.update(user_params) - I18n.locale = current_user.locale - redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg') - else - render :show - end - end - - private - - def after_update_redirect_path - settings_preferences_path - end - - def user_settings - UserSettingsDecorator.new(current_user) - end - - def user_params - params.require(:user).permit( - :locale, - chosen_languages: [] - ) - end - - def user_settings_params - params.require(:user).permit( - :setting_default_privacy, - :setting_default_sensitive, - :setting_default_language, - :setting_unfollow_modal, - :setting_boost_modal, - :setting_favourite_modal, - :setting_delete_modal, - :setting_auto_play_gif, - :setting_display_media, - :setting_expand_spoilers, - :setting_reduce_motion, - :setting_disable_swiping, - :setting_system_font_ui, - :setting_system_emoji_font, - :setting_noindex, - :setting_hide_followers_count, - :setting_aggregate_reblogs, - :setting_show_application, - :setting_advanced_layout, - :setting_default_content_type, - :setting_use_blurhash, - :setting_use_pending_items, - :setting_trends, - :setting_crop_images, - :setting_always_send_emails, - notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag trending_link trending_status appeal), - interactions: %i(must_be_follower must_be_following must_be_following_dm) - ) - end -end diff --git a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb index cbba842a98..0bff01ec27 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -22,18 +22,9 @@ module Settings private - def confirmation_params - params.require(:form_two_factor_confirmation).permit(:otp_attempt) - end - def verify_otp_not_enabled redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled? end - - def acceptable_code? - current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) || - current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt]) - end end end end diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index 7e2d43dcd3..3f9e713572 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -8,9 +8,8 @@ module Settings before_action :require_otp_enabled before_action :require_webauthn_enabled, only: [:index, :destroy] - def new; end - def index; end + def new; end def options current_user.update(webauthn_id: WebAuthn.generate_user_id) unless current_user.webauthn_id @@ -27,7 +26,7 @@ module Settings session[:webauthn_challenge] = options_for_create.challenge - render json: options_for_create, status: :ok + render json: options_for_create, status: 200 end def create @@ -52,7 +51,7 @@ module Settings end else flash[:error] = I18n.t('webauthn_credentials.create.error') - status = :internal_server_error + status = :unprocessable_entity end else flash[:error] = t('webauthn_credentials.create.error') diff --git a/app/controllers/settings/verifications_controller.rb b/app/controllers/settings/verifications_controller.rb new file mode 100644 index 0000000000..fc4f23bb18 --- /dev/null +++ b/app/controllers/settings/verifications_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Settings::VerificationsController < Settings::BaseController + before_action :set_account + + def show + @verified_links = @account.fields.select(&:verified?) + end + + private + + def set_account + @account = current_account + end +end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 0e7bb835f5..3ed1860a00 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -7,6 +7,7 @@ class StatusesCleanupController < ApplicationController before_action :set_policy before_action :set_body_classes before_action :set_pack + before_action :set_cache_headers def show; end @@ -41,4 +42,8 @@ class StatusesCleanupController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_cache_headers + response.cache_control.replace(private: true, no_store: true) + end end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index e5221df3a2..0efafb8456 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -6,14 +6,16 @@ class StatusesController < ApplicationController include Authorization include AccountOwnedConcern + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_status before_action :set_instance_presenter - before_action :set_link_headers before_action :redirect_to_original, only: :show - before_action :set_cache_headers before_action :set_body_classes, only: :embed + after_action :set_link_headers + skip_around_action :set_locale, if: -> { request.format == :json } skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? @@ -28,7 +30,7 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? + expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end @@ -44,7 +46,7 @@ class StatusesController < ApplicationController return not_found if @status.hidden? || @status.reblog? expires_in 180, public: true - response.headers['X-Frame-Options'] = 'ALLOWALL' + response.headers.delete('X-Frame-Options') render layout: 'embedded' end @@ -71,6 +73,6 @@ class StatusesController < ApplicationController end def redirect_to_original - redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? + redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog? end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 65017acba3..7e249dbea5 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -7,11 +7,13 @@ class TagsController < ApplicationController PAGE_SIZE = 20 PAGE_SIZE_MAX = 200 + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :authenticate_user!, if: :whitelist_mode? before_action :set_local before_action :set_tag - before_action :set_statuses + before_action :set_statuses, if: -> { request.format == :rss } before_action :set_instance_presenter skip_before_action :require_functional!, unless: :whitelist_mode? @@ -19,7 +21,7 @@ class TagsController < ApplicationController def show respond_to do |format| format.html do - expires_in 0, public: true unless user_signed_in? + expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? end format.rss do @@ -44,12 +46,7 @@ class TagsController < ApplicationController end def set_statuses - case request.format&.to_sym - when :json - @statuses = cache_collection(TagFeed.new(@tag, current_account, local: @local).get(PAGE_SIZE, params[:max_id], params[:since_id], params[:min_id]), Status) - when :rss - @statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status) - end + @statuses = cache_collection(TagFeed.new(@tag, nil, local: @local).get(limit_param), Status) end def set_instance_presenter @@ -63,9 +60,7 @@ class TagsController < ApplicationController def collection_presenter ActivityPub::CollectionPresenter.new( id: tag_url(@tag), - type: :ordered, - size: @tag.statuses.count, - items: @statuses.map { |status| ActivityPub::TagManager.instance.uri_for(status) } + type: :ordered ) end end diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 2fd6bc7cc9..201da9fbc3 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true module WellKnown - class HostMetaController < ActionController::Base + class HostMetaController < ActionController::Base # rubocop:disable Rails/ApplicationController include RoutingHelper - before_action { response.headers['Vary'] = 'Accept' } - def show @webfinger_template = "#{webfinger_url}?resource={uri}" expires_in 3.days, public: true diff --git a/app/controllers/well_known/nodeinfo_controller.rb b/app/controllers/well_known/nodeinfo_controller.rb index 11a699ebc8..e20e8c62a0 100644 --- a/app/controllers/well_known/nodeinfo_controller.rb +++ b/app/controllers/well_known/nodeinfo_controller.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true module WellKnown - class NodeInfoController < ActionController::Base + class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController include CacheConcern - before_action { response.headers['Vary'] = 'Accept' } + # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` + # and thus re-issuing session cookies + serialization_scope nil def index expires_in 3.days, public: true diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 2b296ea3be..0d897e8e24 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module WellKnown - class WebfingerController < ActionController::Base + class WebfingerController < ActionController::Base # rubocop:disable Rails/ApplicationController include RoutingHelper before_action :set_account @@ -18,7 +18,14 @@ module WellKnown private def set_account - @account = Account.find_local!(username_from_resource) + username = username_from_resource + @account = begin + if username == Rails.configuration.x.local_domain + Account.representative + else + Account.find_local!(username) + end + end end def username_from_resource @@ -34,7 +41,12 @@ module WellKnown end def check_account_suspension - expires_in(3.minutes, public: true) && gone if @account.suspended_permanently? + gone if @account.suspended_permanently? + end + + def gone + expires_in(3.minutes, public: true) + head 410 end def bad_request @@ -46,9 +58,5 @@ module WellKnown expires_in(3.minutes, public: true) head 404 end - - def gone - head 410 - end end end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index e15aee6df1..b8277ee17e 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -28,7 +28,7 @@ module AccountsHelper end def hide_followers_count?(account) - Setting.hide_followers_count || account.user&.setting_hide_followers_count + Setting.hide_followers_count || account.user&.settings&.[]('hide_followers_count') end def account_description(account) diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 215ecea0d7..4018ef6b1c 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -20,7 +20,7 @@ module Admin::ActionLogsHelper when 'Status' link_to log.human_identifier, log.permalink when 'AccountWarning' - link_to log.human_identifier, admin_account_path(log.target_id) + link_to log.human_identifier, disputes_strike_path(log.target_id) when 'Announcement' link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id) when 'IpBlock', 'Instance', 'CustomEmoji' diff --git a/app/helpers/admin/announcements_helper.rb b/app/helpers/admin/announcements_helper.rb deleted file mode 100644 index 0c053ddec3..0000000000 --- a/app/helpers/admin/announcements_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module Admin::AnnouncementsHelper - def time_range(announcement) - if announcement.all_day? - safe_join([l(announcement.starts_at.to_date), ' - ', l(announcement.ends_at.to_date)]) - else - safe_join([l(announcement.starts_at), ' - ', l(announcement.ends_at)]) - end - end -end diff --git a/app/helpers/admin/dashboard_helper.rb b/app/helpers/admin/dashboard_helper.rb index c21d413419..6096ff1381 100644 --- a/app/helpers/admin/dashboard_helper.rb +++ b/app/helpers/admin/dashboard_helper.rb @@ -19,19 +19,17 @@ module Admin::DashboardHelper end def relevant_account_timestamp(account) - timestamp, exact = begin - if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago - [account.user_current_sign_in_at, true] - elsif account.user_current_sign_in_at - [account.user_current_sign_in_at, false] - elsif account.user_pending? - [account.user_created_at, true] - elsif account.last_status_at.present? - [account.last_status_at, true] - else - [nil, false] - end - end + timestamp, exact = if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago + [account.user_current_sign_in_at, true] + elsif account.user_current_sign_in_at + [account.user_current_sign_in_at, false] + elsif account.user_pending? + [account.user_created_at, true] + elsif account.last_status_at.present? + [account.last_status_at, true] + else + [nil, false] + end return '-' if timestamp.nil? return t('generic.today') unless exact diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb index 214c1e2a68..79fee44dc4 100644 --- a/app/helpers/admin/trends/statuses_helper.rb +++ b/app/helpers/admin/trends/statuses_helper.rb @@ -2,13 +2,11 @@ module Admin::Trends::StatusesHelper def one_line_preview(status) - text = begin - if status.local? - status.text.split("\n").first - else - Nokogiri::HTML(status.text).css('html > body > *').first&.text - end - end + text = if status.local? + status.text.split("\n").first + else + Nokogiri::HTML(status.text).css('html > body > *').first&.text + end return '' if text.blank? diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index af453825b3..3148756b75 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -32,10 +32,6 @@ module ApplicationHelper paths.any? { |path| current_page?(path) } ? 'active' : '' end - def active_link_to(label, path, **options) - link_to label, path, options.merge(class: active_nav_class(path)) - end - def show_landing_strip? !user_signed_in? && !single_user_mode? end @@ -56,7 +52,7 @@ module ApplicationHelper if closed_registrations? || omniauth_only? 'https://joinmastodon.org/#getting-started' else - new_user_registration_path + ENV.fetch('SSO_ACCOUNT_SIGN_UP', new_user_registration_path) end end @@ -67,7 +63,7 @@ module ApplicationHelper def link_to_login(name = nil, html_options = nil, &block) target = new_user_session_path - html_options = name if block_given? + html_options = name if block if omniauth_only? && Devise.mappings[:user].omniauthable? && User.omniauth_providers.size == 1 target = omniauth_authorize_path(:user, User.omniauth_providers[0]) @@ -75,7 +71,7 @@ module ApplicationHelper html_options[:method] = :post end - if block_given? + if block link_to(target, html_options, &block) else link_to(name, target, html_options) @@ -105,17 +101,22 @@ module ApplicationHelper def can?(action, record) return false if record.nil? + policy(record).public_send("#{action}?") end def fa_icon(icon, attributes = {}) class_names = attributes[:class]&.split(' ') || [] class_names << 'fa' - class_names += icon.split(' ').map { |cl| "fa-#{cl}" } + class_names += icon.split.map { |cl| "fa-#{cl}" } content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) end + def check_icon + content_tag(:svg, tag.path('fill-rule': 'evenodd', 'clip-rule': 'evenodd', d: 'M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z'), xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 20 20', fill: 'currentColor') + end + def visibility_icon(status) if status.public_visibility? fa_icon('globe', title: I18n.t('statuses.visibilities.public')) @@ -142,34 +143,22 @@ module ApplicationHelper if prefers_autoplay? image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:") else - image_tag(custom_emoji.image.url(:static), class: 'emojione custom-emoji', alt: ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) + image_tag(custom_emoji.image.url(:static), :class => 'emojione custom-emoji', :alt => ":#{custom_emoji.shortcode}", 'data-original' => full_asset_url(custom_emoji.image.url), 'data-static' => full_asset_url(custom_emoji.image.url(:static))) end end def opengraph(property, content) - tag(:meta, content: content, property: property) - end - - def react_component(name, props = {}, &block) - if block.nil? - content_tag(:div, nil, data: { component: name.to_s.camelcase, props: Oj.dump(props) }) - else - content_tag(:div, data: { component: name.to_s.camelcase, props: Oj.dump(props) }, &block) - end - end - - def react_admin_component(name, props = {}) - content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) }) + tag.meta(content: content, property: property) end def body_classes - output = (@body_classes || '').split(' ') + output = body_class_string.split output << "flavour-#{current_flavour.parameterize}" output << "skin-#{current_skin.parameterize}" output << 'system-font' if current_account&.user&.setting_system_font_ui output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion') output << 'rtl' if locale_direction == 'rtl' - output.reject(&:blank?).join(' ') + output.compact_blank.join(' ') end def cdn_host @@ -181,11 +170,11 @@ module ApplicationHelper end def storage_host - "https://#{ENV['S3_ALIAS_HOST'].presence || ENV['S3_CLOUDFRONT_HOST']}" + "https://#{storage_host_var}" end def storage_host? - ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? + storage_host_var.present? end def quote_wrap(text, line_width: 80, break_sequence: "\n") @@ -217,9 +206,7 @@ module ApplicationHelper state_params[:moved_to_account] = current_account.moved_to_account end - if single_user_mode? - state_params[:owner] = Account.local.without_suspended.where('id > 0').first - end + state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode? json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json # rubocop:disable Rails/OutputSafety @@ -245,4 +232,10 @@ module ApplicationHelper def prerender_custom_emojis(html, custom_emojis, other_options = {}) EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s end + + private + + def storage_host_var + ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) + end end diff --git a/app/helpers/branding_helper.rb b/app/helpers/branding_helper.rb index ad7702aea7..2b9c233c23 100644 --- a/app/helpers/branding_helper.rb +++ b/app/helpers/branding_helper.rb @@ -11,11 +11,11 @@ module BrandingHelper end def _logo_as_symbol_wordmark - content_tag(:svg, tag(:use, href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') + content_tag(:svg, tag.use(href: '#logo-symbol-wordmark'), viewBox: '0 0 261 66', class: 'logo logo--wordmark') end def _logo_as_symbol_icon - content_tag(:svg, tag(:use, href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') + content_tag(:svg, tag.use(href: '#logo-symbol-icon'), viewBox: '0 0 79 79', class: 'logo logo--icon') end def render_logo @@ -23,14 +23,12 @@ module BrandingHelper end def render_symbol(version = :icon) - path = begin - case version - when :icon - 'logo-symbol-icon.svg' - when :wordmark - 'logo-symbol-wordmark.svg' - end - end + path = case version + when :icon + 'logo-symbol-icon.svg' + when :wordmark + 'logo-symbol-wordmark.svg' + end render(file: Rails.root.join('app', 'javascript', 'images', path)).html_safe # rubocop:disable Rails/OutputSafety end diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb index ac60cad295..ffcf375ea7 100644 --- a/app/helpers/domain_control_helper.rb +++ b/app/helpers/domain_control_helper.rb @@ -4,13 +4,11 @@ module DomainControlHelper def domain_not_allowed?(uri_or_domain) return if uri_or_domain.blank? - domain = begin - if uri_or_domain.include?('://') - Addressable::URI.parse(uri_or_domain).host - else - uri_or_domain - end - end + domain = if uri_or_domain.include?('://') + Addressable::URI.parse(uri_or_domain).host + else + uri_or_domain + end if whitelist_mode? !DomainAllow.allowed?(domain) diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb index 360783c628..0800601f98 100644 --- a/app/helpers/email_helper.rb +++ b/app/helpers/email_helper.rb @@ -7,7 +7,7 @@ module EmailHelper def email_to_canonical_email(str) username, domain = str.downcase.split('@', 2) - username, = username.gsub('.', '').split('+', 2) + username, = username.delete('.').split('+', 2) "#{username}@#{domain}" end diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 05c003037e..5b2ac1a2ae 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -21,30 +21,26 @@ module FormattingHelper def rss_status_content_format(status) html = status_content_format(status) - before_html = begin - if status.spoiler_text? - tag.p do - tag.strong do - I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) - end + before_html = if status.spoiler_text? + tag.p do + tag.strong do + I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) + end - status.spoiler_text - end + tag.hr - end - end + status.spoiler_text + end + tag.hr + end - after_html = begin - if status.preloadable_poll - tag.p do - safe_join( - status.preloadable_poll.options.map do |o| - tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true) - end, - tag.br - ) - end - end - end + after_html = if status.preloadable_poll + tag.p do + safe_join( + status.preloadable_poll.options.map do |o| + tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true) + end, + tag.br + ) + end + end prerender_custom_emojis( safe_join([before_html, html, after_html]), diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index f41104709e..c5b83326db 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -8,7 +8,7 @@ module HomeHelper end def account_link_to(account, button = '', path: nil) - content_tag(:div, class: 'account') do + content_tag(:div, class: 'account account--minimal') do content_tag(:div, class: 'account__wrapper') do section = if account.nil? content_tag(:div, class: 'account__display-name') do @@ -41,9 +41,9 @@ module HomeHelper def obscured_counter(count) if count <= 0 - 0 + '0' elsif count == 1 - 1 + '1' else '1+' end @@ -57,14 +57,6 @@ module HomeHelper end end - def optional_link_to(condition, path, options = {}, &block) - if condition - link_to(path, options, &block) - else - content_tag(:div, &block) - end - end - def sign_up_message if closed_registrations? t('auth.registration_closed', instance: site_hostname) diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index daacb535b6..893afdd51f 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -9,15 +9,17 @@ module InstanceHelper @site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host end - def description_for_sign_up - prefix = begin - if @invite.present? - I18n.t('auth.description.prefix_invited_by_user', name: @invite.user.account.username) - else - I18n.t('auth.description.prefix_sign_up') - end - end + def description_for_sign_up(invite = nil) + safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ') + end - safe_join([prefix, I18n.t('auth.description.suffix')], ' ') + private + + def description_prefix(invite) + if invite.present? + I18n.t('auth.description.prefix_invited_by_user', name: invite.user.account.username) + else + I18n.t('auth.description.prefix_sign_up') + end end end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index 102e4b1328..ce3ff094f6 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -26,15 +26,13 @@ module JsonLdHelper # The url attribute can be a string, an array of strings, or an array of objects. # The objects could include a mimeType. Not-included mimeType means it's text/html. def url_to_href(value, preferred_type = nil) - single_value = begin - if value.is_a?(Array) && !value.first.is_a?(String) - value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } - elsif value.is_a?(Array) - value.first - else - value - end - end + single_value = if value.is_a?(Array) && !value.first.is_a?(String) + value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) } + elsif value.is_a?(Array) + value.first + else + value + end if single_value.nil? || single_value.is_a?(String) single_value @@ -65,11 +63,11 @@ module JsonLdHelper uri.nil? || !uri.start_with?('http://', 'https://') end - def invalid_origin?(url) - return true if unsupported_uri_scheme?(url) + def non_matching_uri_hosts?(base_url, comparison_url) + return true if unsupported_uri_scheme?(comparison_url) - needle = Addressable::URI.parse(url).host - haystack = Addressable::URI.parse(@account.uri).host + needle = Addressable::URI.parse(comparison_url).host + haystack = Addressable::URI.parse(base_url).host !haystack.casecmp(needle).zero? end @@ -213,7 +211,7 @@ module JsonLdHelper end end - def load_jsonld_context(url, _options = {}, &_block) + def load_jsonld_context(url, _options = {}, &block) json = Rails.cache.fetch("jsonld:context:#{url}", expires_in: 30.days, raw: true) do request = Request.new(:get, url) request.add_headers('Accept' => 'application/ld+json') @@ -226,6 +224,6 @@ module JsonLdHelper doc = JSON::LD::API::RemoteDocument.new(json, documentUrl: url) - block_given? ? yield(doc) : doc + block ? yield(doc) : doc end end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index bb87dd596c..840a18d3e6 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -# rubocop:disable Metrics/ModuleLength, Style/WordArray module LanguagesHelper ISO_639_1 = { @@ -199,6 +198,7 @@ module LanguagesHelper sco: ['Scots', 'Scots'].freeze, sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze, + szl: ['Silesian', 'ślůnsko godka'].freeze, tok: ['Toki Pona', 'toki pona'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, @@ -210,8 +210,10 @@ module LanguagesHelper # names, but for some translations, we need the names of the # regional variants specifically REGIONAL_LOCALE_NAMES = { + 'en-GB': 'English (British)', 'es-AR': 'Español (Argentina)', 'es-MX': 'Español (México)', + 'fr-QC': 'Français (Canadien)', 'pt-BR': 'Português (Brasil)', 'pt-PT': 'Português (Portugal)', 'sr-Latn': 'Srpski (latinica)', @@ -270,4 +272,4 @@ module LanguagesHelper end end -# rubocop:enable Metrics/ModuleLength, Style/WordArray +# rubocop:enable Metrics/ModuleLength diff --git a/app/helpers/media_component_helper.rb b/app/helpers/media_component_helper.rb new file mode 100644 index 0000000000..a57d0b4b62 --- /dev/null +++ b/app/helpers/media_component_helper.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module MediaComponentHelper + def render_video_component(status, **options) + video = status.ordered_media_attachments.first + + meta = video.file.meta || {} + + component_params = { + sensitive: sensitive_viewer?(status, current_account), + src: full_asset_url(video.file.url(:original)), + preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), + alt: video.description, + blurhash: video.blurhash, + frameRate: meta.dig('original', 'frame_rate'), + inline: true, + media: [ + serialize_media_attachment(video), + ].as_json, + }.merge(**options) + + react_component :video, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_audio_component(status, **options) + audio = status.ordered_media_attachments.first + + meta = audio.file.meta || {} + + component_params = { + src: full_asset_url(audio.file.url(:original)), + poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), + alt: audio.description, + backgroundColor: meta.dig('colors', 'background'), + foregroundColor: meta.dig('colors', 'foreground'), + accentColor: meta.dig('colors', 'accent'), + duration: meta.dig('original', 'duration'), + }.merge(**options) + + react_component :audio, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_media_gallery_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + autoplay: prefers_autoplay?, + media: status.ordered_media_attachments.map { |a| serialize_media_attachment(a).as_json }, + }.merge(**options) + + react_component :media_gallery, component_params do + render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } + end + end + + def render_card_component(status, **options) + component_params = { + sensitive: sensitive_viewer?(status, current_account), + card: serialize_status_card(status).as_json, + }.merge(**options) + + react_component :card, component_params + end + + def render_poll_component(status, **options) + component_params = { + disabled: true, + poll: serialize_status_poll(status).as_json, + }.merge(**options) + + react_component :poll, component_params do + render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } + end + end + + private + + def serialize_media_attachment(attachment) + ActiveModelSerializers::SerializableResource.new( + attachment, + serializer: REST::MediaAttachmentSerializer + ) + end + + def serialize_status_card(status) + ActiveModelSerializers::SerializableResource.new( + status.preview_card, + serializer: REST::PreviewCardSerializer + ) + end + + def serialize_status_poll(status) + ActiveModelSerializers::SerializableResource.new( + status.preloadable_poll, + serializer: REST::PollSerializer, + scope: current_user, + scope_name: :current_user + ) + end + + def sensitive_viewer?(status, account) + if !account.nil? && account.id == status.account_id + status.sensitive + else + status.account.sensitized? || status.sensitive + end + end +end diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb new file mode 100644 index 0000000000..ce616e8306 --- /dev/null +++ b/app/helpers/react_component_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module ReactComponentHelper + def react_component(name, props = {}, &block) + data = { component: name.to_s.camelcase, props: Oj.dump(props) } + if block.nil? + div_tag_with_data(data) + else + content_tag(:div, data: data, &block) + end + end + + def react_admin_component(name, props = {}) + data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) } + div_tag_with_data(data) + end + + private + + def div_tag_with_data(data) + content_tag(:div, nil, data: data) + end +end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 3d5592867c..889ca7f402 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,10 +5,6 @@ module SettingsHelper LanguagesHelper::SUPPORTED_LOCALES.keys end - def hash_to_object(hash) - HashObject.new(hash) - end - def session_device_icon(session) device = session.detection.device @@ -28,13 +24,4 @@ module SettingsHelper safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ') end end - - def picture_hint(hint, picture) - if picture.original_filename.nil? - hint - else - link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete } - safe_join([hint, link], '
'.html_safe) - end - end end diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index d1e3fddafe..f1f1ea872e 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -51,14 +51,14 @@ module StatusesHelper end def status_description(status) - components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] + components = [[media_summary(status), status_text_summary(status)].compact_blank.join(' · ')] if status.spoiler_text.blank? components << status.text components << poll_summary(status) end - components.reject(&:blank?).join("\n\n") + components.compact_blank.join("\n\n") end def stream_link_target @@ -105,94 +105,10 @@ module StatusesHelper end end - def sensitized?(status, account) - if !account.nil? && account.id == status.account_id - status.sensitive - else - status.account.sensitized? || status.sensitive - end - end - def embedded_view? params[:controller] == EMBEDDED_CONTROLLER && params[:action] == EMBEDDED_ACTION end - def render_video_component(status, **options) - video = status.ordered_media_attachments.first - - meta = video.file.meta || {} - - component_params = { - sensitive: sensitized?(status, current_account), - src: full_asset_url(video.file.url(:original)), - preview: full_asset_url(video.thumbnail.present? ? video.thumbnail.url : video.file.url(:small)), - alt: video.description, - blurhash: video.blurhash, - frameRate: meta.dig('original', 'frame_rate'), - inline: true, - media: [ - ActiveModelSerializers::SerializableResource.new(video, serializer: REST::MediaAttachmentSerializer), - ].as_json, - }.merge(**options) - - react_component :video, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_audio_component(status, **options) - audio = status.ordered_media_attachments.first - - meta = audio.file.meta || {} - - component_params = { - src: full_asset_url(audio.file.url(:original)), - poster: full_asset_url(audio.thumbnail.present? ? audio.thumbnail.url : status.account.avatar_static_url), - alt: audio.description, - backgroundColor: meta.dig('colors', 'background'), - foregroundColor: meta.dig('colors', 'foreground'), - accentColor: meta.dig('colors', 'accent'), - duration: meta.dig('original', 'duration'), - }.merge(**options) - - react_component :audio, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_media_gallery_component(status, **options) - component_params = { - sensitive: sensitized?(status, current_account), - autoplay: prefers_autoplay?, - media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }, - }.merge(**options) - - react_component :media_gallery, component_params do - render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments } - end - end - - def render_card_component(status, **options) - component_params = { - sensitive: sensitized?(status, current_account), - maxDescription: 160, - card: ActiveModelSerializers::SerializableResource.new(status.preview_card, serializer: REST::PreviewCardSerializer).as_json, - }.merge(**options) - - react_component :card, component_params - end - - def render_poll_component(status, **options) - component_params = { - disabled: true, - poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json, - }.merge(**options) - - react_component :poll, component_params do - render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? } - end - end - def prefers_autoplay? ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif end diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index ac1b2f95fb..97b2f4e309 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -2,6 +2,7 @@ import 'packs/public-path'; import { delegate } from '@rails/ujs'; + import ready from '../mastodon/ready'; const setAnnouncementEndsAttributes = (target) => { diff --git a/app/javascript/core/embed.js b/app/javascript/core/embed.js index 9083eb7a3e..d1e8f6b108 100644 --- a/app/javascript/core/embed.js +++ b/app/javascript/core/embed.js @@ -15,7 +15,7 @@ window.addEventListener('message', e => { id: data.id, height: document.getElementsByTagName('html')[0].scrollHeight, }, '*'); - }; + } if (['interactive', 'complete'].includes(document.readyState)) { setEmbedHeight(); diff --git a/app/javascript/core/mailer.js b/app/javascript/core/mailer.js index a4b6d54464..a2ad5e73ac 100644 --- a/app/javascript/core/mailer.js +++ b/app/javascript/core/mailer.js @@ -1,3 +1,3 @@ -require('../styles/mailer.scss'); +import '../styles/mailer.scss'; require.context('../icons'); diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js index 5c7a51f447..01b4157f8c 100644 --- a/app/javascript/core/public.js +++ b/app/javascript/core/public.js @@ -1,10 +1,8 @@ // This file will be loaded on public pages, regardless of theme. import 'packs/public-path'; -import ready from '../mastodon/ready'; -const { delegate } = require('@rails/ujs'); -const { length } = require('stringz'); +import { delegate } from '@rails/ujs'; const getProfileAvatarAnimationHandler = (swapTo) => { //animate avatar gifs on the profile page when moused over diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index d5bb9532c4..40537377c0 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -1,8 +1,10 @@ // This file will be loaded on settings pages, regardless of theme. import 'packs/public-path'; +import { delegate } from '@rails/ujs'; import escapeTextContentForBrowser from 'escape-html'; -const { delegate } = require('@rails/ujs'); + + import emojify from '../mastodon/features/emoji/emoji'; delegate(document, '#account_display_name', 'input', ({ target }) => { @@ -65,7 +67,7 @@ delegate(document, '.input-copy button', 'click', ({ target }) => { input.blur(); target.parentNode.classList.add('copied'); - setTimeout(() => { + setTimeout(() => { target.parentNode.classList.remove('copied'); }, 700); } diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index b9144e43aa..30676dcf58 100644 --- a/app/javascript/core/theme.yml +++ b/app/javascript/core/theme.yml @@ -16,4 +16,5 @@ pack: modal: public.js public: public.js settings: settings.js + sign_up: share: diff --git a/app/javascript/core/two_factor_authentication.js b/app/javascript/core/two_factor_authentication.js index f076cdf30a..e76700a480 100644 --- a/app/javascript/core/two_factor_authentication.js +++ b/app/javascript/core/two_factor_authentication.js @@ -1,6 +1,8 @@ import 'packs/public-path'; -import axios from 'axios'; + import * as WebAuthnJSON from '@github/webauthn-json'; +import axios from 'axios'; + import ready from '../mastodon/ready'; import 'regenerator-runtime/runtime'; diff --git a/app/javascript/flavours/glitch/actions/account_notes.js b/app/javascript/flavours/glitch/actions/account_notes.js index 059ed9e803..62a6b4cbb8 100644 --- a/app/javascript/flavours/glitch/actions/account_notes.js +++ b/app/javascript/flavours/glitch/actions/account_notes.js @@ -21,27 +21,27 @@ export function submitAccountNote() { dispatch(submitAccountNoteSuccess(response.data)); }).catch(error => dispatch(submitAccountNoteFail(error))); }; -}; +} export function submitAccountNoteRequest() { return { type: ACCOUNT_NOTE_SUBMIT_REQUEST, }; -}; +} export function submitAccountNoteSuccess(relationship) { return { type: ACCOUNT_NOTE_SUBMIT_SUCCESS, relationship, }; -}; +} export function submitAccountNoteFail(error) { return { type: ACCOUNT_NOTE_SUBMIT_FAIL, error, }; -}; +} export function initEditAccountNote(account) { return (dispatch, getState) => { @@ -53,17 +53,17 @@ export function initEditAccountNote(account) { comment, }); }; -}; +} export function cancelAccountNote() { return { type: ACCOUNT_NOTE_CANCEL, }; -}; +} export function changeAccountNoteComment(comment) { return { type: ACCOUNT_NOTE_CHANGE_COMMENT, comment, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index dc670e50ac..d4f18ff2d7 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -1,5 +1,6 @@ import api, { getLinks } from '../api'; -import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; + +import { importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; @@ -81,7 +82,10 @@ export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST'; export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS'; export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL'; -export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS'; +export const PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL = 'PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL'; + export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR'; export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE'; @@ -108,7 +112,7 @@ export function fetchAccount(id) { dispatch(fetchAccountFail(id, error)); }); }; -}; +} export const lookupAccount = acct => (dispatch, getState) => { dispatch(lookupAccountRequest(acct)); @@ -143,13 +147,13 @@ export function fetchAccountRequest(id) { type: ACCOUNT_FETCH_REQUEST, id, }; -}; +} export function fetchAccountSuccess() { return { type: ACCOUNT_FETCH_SUCCESS, }; -}; +} export function fetchAccountFail(id, error) { return { @@ -158,7 +162,7 @@ export function fetchAccountFail(id, error) { error, skipAlert: true, }; -}; +} export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { @@ -173,7 +177,7 @@ export function followAccount(id, options = { reblogs: true }) { dispatch(followAccountFail(error, locked)); }); }; -}; +} export function unfollowAccount(id) { return (dispatch, getState) => { @@ -185,7 +189,7 @@ export function unfollowAccount(id) { dispatch(unfollowAccountFail(error)); }); }; -}; +} export function followAccountRequest(id, locked) { return { @@ -194,7 +198,7 @@ export function followAccountRequest(id, locked) { locked, skipLoading: true, }; -}; +} export function followAccountSuccess(relationship, alreadyFollowing) { return { @@ -203,7 +207,7 @@ export function followAccountSuccess(relationship, alreadyFollowing) { alreadyFollowing, skipLoading: true, }; -}; +} export function followAccountFail(error, locked) { return { @@ -212,7 +216,7 @@ export function followAccountFail(error, locked) { locked, skipLoading: true, }; -}; +} export function unfollowAccountRequest(id) { return { @@ -220,7 +224,7 @@ export function unfollowAccountRequest(id) { id, skipLoading: true, }; -}; +} export function unfollowAccountSuccess(relationship, statuses) { return { @@ -229,7 +233,7 @@ export function unfollowAccountSuccess(relationship, statuses) { statuses, skipLoading: true, }; -}; +} export function unfollowAccountFail(error) { return { @@ -237,7 +241,7 @@ export function unfollowAccountFail(error) { error, skipLoading: true, }; -}; +} export function blockAccount(id) { return (dispatch, getState) => { @@ -250,7 +254,7 @@ export function blockAccount(id) { dispatch(blockAccountFail(id, error)); }); }; -}; +} export function unblockAccount(id) { return (dispatch, getState) => { @@ -262,14 +266,14 @@ export function unblockAccount(id) { dispatch(unblockAccountFail(id, error)); }); }; -}; +} export function blockAccountRequest(id) { return { type: ACCOUNT_BLOCK_REQUEST, id, }; -}; +} export function blockAccountSuccess(relationship, statuses) { return { @@ -277,35 +281,35 @@ export function blockAccountSuccess(relationship, statuses) { relationship, statuses, }; -}; +} export function blockAccountFail(error) { return { type: ACCOUNT_BLOCK_FAIL, error, }; -}; +} export function unblockAccountRequest(id) { return { type: ACCOUNT_UNBLOCK_REQUEST, id, }; -}; +} export function unblockAccountSuccess(relationship) { return { type: ACCOUNT_UNBLOCK_SUCCESS, relationship, }; -}; +} export function unblockAccountFail(error) { return { type: ACCOUNT_UNBLOCK_FAIL, error, }; -}; +} export function muteAccount(id, notifications, duration=0) { @@ -319,7 +323,7 @@ export function muteAccount(id, notifications, duration=0) { dispatch(muteAccountFail(id, error)); }); }; -}; +} export function unmuteAccount(id) { return (dispatch, getState) => { @@ -331,14 +335,14 @@ export function unmuteAccount(id) { dispatch(unmuteAccountFail(id, error)); }); }; -}; +} export function muteAccountRequest(id) { return { type: ACCOUNT_MUTE_REQUEST, id, }; -}; +} export function muteAccountSuccess(relationship, statuses) { return { @@ -346,35 +350,35 @@ export function muteAccountSuccess(relationship, statuses) { relationship, statuses, }; -}; +} export function muteAccountFail(error) { return { type: ACCOUNT_MUTE_FAIL, error, }; -}; +} export function unmuteAccountRequest(id) { return { type: ACCOUNT_UNMUTE_REQUEST, id, }; -}; +} export function unmuteAccountSuccess(relationship) { return { type: ACCOUNT_UNMUTE_SUCCESS, relationship, }; -}; +} export function unmuteAccountFail(error) { return { type: ACCOUNT_UNMUTE_FAIL, error, }; -}; +} export function fetchFollowers(id) { @@ -391,14 +395,14 @@ export function fetchFollowers(id) { dispatch(fetchFollowersFail(id, error)); }); }; -}; +} export function fetchFollowersRequest(id) { return { type: FOLLOWERS_FETCH_REQUEST, id, }; -}; +} export function fetchFollowersSuccess(id, accounts, next) { return { @@ -407,7 +411,7 @@ export function fetchFollowersSuccess(id, accounts, next) { accounts, next, }; -}; +} export function fetchFollowersFail(id, error) { return { @@ -416,7 +420,7 @@ export function fetchFollowersFail(id, error) { error, skipNotFound: true, }; -}; +} export function expandFollowers(id) { return (dispatch, getState) => { @@ -438,14 +442,14 @@ export function expandFollowers(id) { dispatch(expandFollowersFail(id, error)); }); }; -}; +} export function expandFollowersRequest(id) { return { type: FOLLOWERS_EXPAND_REQUEST, id, }; -}; +} export function expandFollowersSuccess(id, accounts, next) { return { @@ -454,7 +458,7 @@ export function expandFollowersSuccess(id, accounts, next) { accounts, next, }; -}; +} export function expandFollowersFail(id, error) { return { @@ -462,7 +466,7 @@ export function expandFollowersFail(id, error) { id, error, }; -}; +} export function fetchFollowing(id) { return (dispatch, getState) => { @@ -478,14 +482,14 @@ export function fetchFollowing(id) { dispatch(fetchFollowingFail(id, error)); }); }; -}; +} export function fetchFollowingRequest(id) { return { type: FOLLOWING_FETCH_REQUEST, id, }; -}; +} export function fetchFollowingSuccess(id, accounts, next) { return { @@ -494,7 +498,7 @@ export function fetchFollowingSuccess(id, accounts, next) { accounts, next, }; -}; +} export function fetchFollowingFail(id, error) { return { @@ -503,7 +507,7 @@ export function fetchFollowingFail(id, error) { error, skipNotFound: true, }; -}; +} export function expandFollowing(id) { return (dispatch, getState) => { @@ -525,14 +529,14 @@ export function expandFollowing(id) { dispatch(expandFollowingFail(id, error)); }); }; -}; +} export function expandFollowingRequest(id) { return { type: FOLLOWING_EXPAND_REQUEST, id, }; -}; +} export function expandFollowingSuccess(id, accounts, next) { return { @@ -541,7 +545,7 @@ export function expandFollowingSuccess(id, accounts, next) { accounts, next, }; -}; +} export function expandFollowingFail(id, error) { return { @@ -549,7 +553,7 @@ export function expandFollowingFail(id, error) { id, error, }; -}; +} export function fetchRelationships(accountIds) { return (dispatch, getState) => { @@ -570,7 +574,7 @@ export function fetchRelationships(accountIds) { dispatch(fetchRelationshipsFail(error)); }); }; -}; +} export function fetchRelationshipsRequest(ids) { return { @@ -578,7 +582,7 @@ export function fetchRelationshipsRequest(ids) { ids, skipLoading: true, }; -}; +} export function fetchRelationshipsSuccess(relationships) { return { @@ -586,7 +590,7 @@ export function fetchRelationshipsSuccess(relationships) { relationships, skipLoading: true, }; -}; +} export function fetchRelationshipsFail(error) { return { @@ -595,7 +599,7 @@ export function fetchRelationshipsFail(error) { skipLoading: true, skipNotFound: true, }; -}; +} export function fetchFollowRequests() { return (dispatch, getState) => { @@ -607,13 +611,13 @@ export function fetchFollowRequests() { dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null)); }).catch(error => dispatch(fetchFollowRequestsFail(error))); }; -}; +} export function fetchFollowRequestsRequest() { return { type: FOLLOW_REQUESTS_FETCH_REQUEST, }; -}; +} export function fetchFollowRequestsSuccess(accounts, next) { return { @@ -621,14 +625,14 @@ export function fetchFollowRequestsSuccess(accounts, next) { accounts, next, }; -}; +} export function fetchFollowRequestsFail(error) { return { type: FOLLOW_REQUESTS_FETCH_FAIL, error, }; -}; +} export function expandFollowRequests() { return (dispatch, getState) => { @@ -646,13 +650,13 @@ export function expandFollowRequests() { dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null)); }).catch(error => dispatch(expandFollowRequestsFail(error))); }; -}; +} export function expandFollowRequestsRequest() { return { type: FOLLOW_REQUESTS_EXPAND_REQUEST, }; -}; +} export function expandFollowRequestsSuccess(accounts, next) { return { @@ -660,14 +664,14 @@ export function expandFollowRequestsSuccess(accounts, next) { accounts, next, }; -}; +} export function expandFollowRequestsFail(error) { return { type: FOLLOW_REQUESTS_EXPAND_FAIL, error, }; -}; +} export function authorizeFollowRequest(id) { return (dispatch, getState) => { @@ -678,21 +682,21 @@ export function authorizeFollowRequest(id) { .then(() => dispatch(authorizeFollowRequestSuccess(id))) .catch(error => dispatch(authorizeFollowRequestFail(id, error))); }; -}; +} export function authorizeFollowRequestRequest(id) { return { type: FOLLOW_REQUEST_AUTHORIZE_REQUEST, id, }; -}; +} export function authorizeFollowRequestSuccess(id) { return { type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS, id, }; -}; +} export function authorizeFollowRequestFail(id, error) { return { @@ -700,7 +704,7 @@ export function authorizeFollowRequestFail(id, error) { id, error, }; -}; +} export function rejectFollowRequest(id) { @@ -712,21 +716,21 @@ export function rejectFollowRequest(id) { .then(() => dispatch(rejectFollowRequestSuccess(id))) .catch(error => dispatch(rejectFollowRequestFail(id, error))); }; -}; +} export function rejectFollowRequestRequest(id) { return { type: FOLLOW_REQUEST_REJECT_REQUEST, id, }; -}; +} export function rejectFollowRequestSuccess(id) { return { type: FOLLOW_REQUEST_REJECT_SUCCESS, id, }; -}; +} export function rejectFollowRequestFail(id, error) { return { @@ -734,7 +738,7 @@ export function rejectFollowRequestFail(id, error) { id, error, }; -}; +} export function pinAccount(id) { return (dispatch, getState) => { @@ -746,7 +750,7 @@ export function pinAccount(id) { dispatch(pinAccountFail(error)); }); }; -}; +} export function unpinAccount(id) { return (dispatch, getState) => { @@ -758,49 +762,49 @@ export function unpinAccount(id) { dispatch(unpinAccountFail(error)); }); }; -}; +} export function pinAccountRequest(id) { return { type: ACCOUNT_PIN_REQUEST, id, }; -}; +} export function pinAccountSuccess(relationship) { return { type: ACCOUNT_PIN_SUCCESS, relationship, }; -}; +} export function pinAccountFail(error) { return { type: ACCOUNT_PIN_FAIL, error, }; -}; +} export function unpinAccountRequest(id) { return { type: ACCOUNT_UNPIN_REQUEST, id, }; -}; +} export function unpinAccountSuccess(relationship) { return { type: ACCOUNT_UNPIN_SUCCESS, relationship, }; -}; +} export function unpinAccountFail(error) { return { type: ACCOUNT_UNPIN_FAIL, error, }; -}; +} export const revealAccount = id => ({ type: ACCOUNT_REVEAL, @@ -811,18 +815,18 @@ export function fetchPinnedAccounts() { return (dispatch, getState) => { dispatch(fetchPinnedAccountsRequest()); - api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } }).then(response => { + api(getState).get('/api/v1/endorsements', { params: { limit: 0 } }).then(response => { dispatch(importFetchedAccounts(response.data)); dispatch(fetchPinnedAccountsSuccess(response.data)); }).catch(err => dispatch(fetchPinnedAccountsFail(err))); }; -}; +} export function fetchPinnedAccountsRequest() { return { type: PINNED_ACCOUNTS_FETCH_REQUEST, }; -}; +} export function fetchPinnedAccountsSuccess(accounts, next) { return { @@ -830,17 +834,19 @@ export function fetchPinnedAccountsSuccess(accounts, next) { accounts, next, }; -}; +} export function fetchPinnedAccountsFail(error) { return { type: PINNED_ACCOUNTS_FETCH_FAIL, error, }; -}; +} export function fetchPinnedAccountsSuggestions(q) { return (dispatch, getState) => { + dispatch(fetchPinnedAccountsSuggestionsRequest()); + const params = { q, resolve: false, @@ -850,35 +856,48 @@ export function fetchPinnedAccountsSuggestions(q) { api(getState).get('/api/v1/accounts/search', { params }).then(response => { dispatch(importFetchedAccounts(response.data)); - dispatch(fetchPinnedAccountsSuggestionsReady(q, response.data)); - }); + dispatch(fetchPinnedAccountsSuggestionsSuccess(q, response.data)); + }).catch(err => dispatch(fetchPinnedAccountsSuggestionsFail(err))); }; -}; +} -export function fetchPinnedAccountsSuggestionsReady(query, accounts) { +export function fetchPinnedAccountsSuggestionsRequest() { return { - type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY, + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST, + }; +} + +export function fetchPinnedAccountsSuggestionsSuccess(query, accounts) { + return { + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS, query, accounts, }; -}; +} + +export function fetchPinnedAccountsSuggestionsFail(error) { + return { + type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL, + error, + }; +} export function clearPinnedAccountsSuggestions() { return { type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR, }; -}; +} export function changePinnedAccountsSuggestions(value) { return { type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE, value, - } -}; + }; +} export function resetPinnedAccountsEditor() { return { type: PINNED_ACCOUNTS_EDITOR_RESET, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/alerts.js b/app/javascript/flavours/glitch/actions/alerts.js index 1670f9c10d..0220b0af58 100644 --- a/app/javascript/flavours/glitch/actions/alerts.js +++ b/app/javascript/flavours/glitch/actions/alerts.js @@ -17,13 +17,13 @@ export function dismissAlert(alert) { type: ALERT_DISMISS, alert, }; -}; +} export function clearAlert() { return { type: ALERT_CLEAR, }; -}; +} export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) { return { @@ -32,7 +32,7 @@ export function showAlert(title = messages.unexpectedTitle, message = messages.u message, message_values, }; -}; +} export function showAlertForError(error, skipNotFound = false) { if (error.response) { diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index 586dcfd337..339c5f3adc 100644 --- a/app/javascript/flavours/glitch/actions/announcements.js +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -1,4 +1,5 @@ import api from '../api'; + import { normalizeAnnouncement } from './importer/normalizer'; export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/app.js b/app/javascript/flavours/glitch/actions/app.js deleted file mode 100644 index de2d93e292..0000000000 --- a/app/javascript/flavours/glitch/actions/app.js +++ /dev/null @@ -1,6 +0,0 @@ -export const APP_LAYOUT_CHANGE = 'APP_LAYOUT_CHANGE'; - -export const changeLayout = layout => ({ - type: APP_LAYOUT_CHANGE, - layout, -}); diff --git a/app/javascript/flavours/glitch/actions/app.ts b/app/javascript/flavours/glitch/actions/app.ts new file mode 100644 index 0000000000..6fbfc07f68 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/app.ts @@ -0,0 +1,9 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { LayoutType } from '../is_mobile'; + +interface ChangeLayoutPayload { + layout: LayoutType; +} +export const changeLayout = + createAction('APP_LAYOUT_CHANGE'); diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js index fd9881302a..e293657ad3 100644 --- a/app/javascript/flavours/glitch/actions/blocks.js +++ b/app/javascript/flavours/glitch/actions/blocks.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; import { openModal } from './modal'; @@ -24,13 +25,13 @@ export function fetchBlocks() { dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => dispatch(fetchBlocksFail(error))); }; -}; +} export function fetchBlocksRequest() { return { type: BLOCKS_FETCH_REQUEST, }; -}; +} export function fetchBlocksSuccess(accounts, next) { return { @@ -38,14 +39,14 @@ export function fetchBlocksSuccess(accounts, next) { accounts, next, }; -}; +} export function fetchBlocksFail(error) { return { type: BLOCKS_FETCH_FAIL, error, }; -}; +} export function expandBlocks() { return (dispatch, getState) => { @@ -64,13 +65,13 @@ export function expandBlocks() { dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => dispatch(expandBlocksFail(error))); }; -}; +} export function expandBlocksRequest() { return { type: BLOCKS_EXPAND_REQUEST, }; -}; +} export function expandBlocksSuccess(accounts, next) { return { @@ -78,14 +79,14 @@ export function expandBlocksSuccess(accounts, next) { accounts, next, }; -}; +} export function expandBlocksFail(error) { return { type: BLOCKS_EXPAND_FAIL, error, }; -}; +} export function initBlockModal(account) { return dispatch => { @@ -94,6 +95,6 @@ export function initBlockModal(account) { account, }); - dispatch(openModal('BLOCK')); + dispatch(openModal({ modalType: 'BLOCK' })); }; } diff --git a/app/javascript/flavours/glitch/actions/bookmarks.js b/app/javascript/flavours/glitch/actions/bookmarks.js index 544ed2ff22..0b16f61e63 100644 --- a/app/javascript/flavours/glitch/actions/bookmarks.js +++ b/app/javascript/flavours/glitch/actions/bookmarks.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; @@ -25,13 +26,13 @@ export function fetchBookmarkedStatuses() { dispatch(fetchBookmarkedStatusesFail(error)); }); }; -}; +} export function fetchBookmarkedStatusesRequest() { return { type: BOOKMARKED_STATUSES_FETCH_REQUEST, }; -}; +} export function fetchBookmarkedStatusesSuccess(statuses, next) { return { @@ -39,14 +40,14 @@ export function fetchBookmarkedStatusesSuccess(statuses, next) { statuses, next, }; -}; +} export function fetchBookmarkedStatusesFail(error) { return { type: BOOKMARKED_STATUSES_FETCH_FAIL, error, }; -}; +} export function expandBookmarkedStatuses() { return (dispatch, getState) => { @@ -66,13 +67,13 @@ export function expandBookmarkedStatuses() { dispatch(expandBookmarkedStatusesFail(error)); }); }; -}; +} export function expandBookmarkedStatusesRequest() { return { type: BOOKMARKED_STATUSES_EXPAND_REQUEST, }; -}; +} export function expandBookmarkedStatusesSuccess(statuses, next) { return { @@ -80,11 +81,11 @@ export function expandBookmarkedStatusesSuccess(statuses, next) { statuses, next, }; -}; +} export function expandBookmarkedStatusesFail(error) { return { type: BOOKMARKED_STATUSES_EXPAND_FAIL, error, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/boosts.js b/app/javascript/flavours/glitch/actions/boosts.js index 6e14065d6f..1fc2e391e2 100644 --- a/app/javascript/flavours/glitch/actions/boosts.js +++ b/app/javascript/flavours/glitch/actions/boosts.js @@ -11,10 +11,13 @@ export function initBoostModal(props) { dispatch({ type: BOOSTS_INIT_MODAL, - privacy + privacy, }); - dispatch(openModal('BOOST', props)); + dispatch(openModal({ + modalType: 'BOOST', + modalProps: props, + })); }; } diff --git a/app/javascript/flavours/glitch/actions/columns.js b/app/javascript/flavours/glitch/actions/columns.js index 9b87415fb8..302c3f0f9b 100644 --- a/app/javascript/flavours/glitch/actions/columns.js +++ b/app/javascript/flavours/glitch/actions/columns.js @@ -15,7 +15,7 @@ export function addColumn(id, params) { dispatch(saveSettings()); }; -}; +} export function removeColumn(uuid) { return dispatch => { @@ -26,7 +26,7 @@ export function removeColumn(uuid) { dispatch(saveSettings()); }; -}; +} export function moveColumn(uuid, direction) { return dispatch => { @@ -38,7 +38,7 @@ export function moveColumn(uuid, direction) { dispatch(saveSettings()); }; -}; +} export function changeColumnParams(uuid, path, value) { return dispatch => { diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index d1f27ddb74..b50fee5a7c 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -1,11 +1,14 @@ +import { defineMessages } from 'react-intl'; + import axios from 'axios'; import { throttle } from 'lodash'; -import { defineMessages } from 'react-intl'; + import api from 'flavours/glitch/api'; import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light'; import { tagHistory } from 'flavours/glitch/settings'; import { recoverHashtags } from 'flavours/glitch/utils/hashtag'; import resizeImage from 'flavours/glitch/utils/resize_image'; + import { showAlert, showAlertForError } from './alerts'; import { useEmoji } from './emojis'; import { importFetchedAccounts, importFetchedStatus } from './importer'; @@ -101,14 +104,14 @@ export function setComposeToStatus(status, text, spoiler_text, content_type) { spoiler_text, content_type, }; -}; +} export function changeCompose(text) { return { type: COMPOSE_CHANGE, text: text, }; -}; +} export function replyCompose(status, routerHistory) { return (dispatch, getState) => { @@ -121,19 +124,19 @@ export function replyCompose(status, routerHistory) { ensureComposeIsVisible(getState, routerHistory); }; -}; +} export function cancelReplyCompose() { return { type: COMPOSE_REPLY_CANCEL, }; -}; +} export function resetCompose() { return { type: COMPOSE_RESET, }; -}; +} export function mentionCompose(account, routerHistory) { return (dispatch, getState) => { @@ -144,7 +147,7 @@ export function mentionCompose(account, routerHistory) { ensureComposeIsVisible(getState, routerHistory); }; -}; +} export function directCompose(account, routerHistory) { return (dispatch, getState) => { @@ -155,7 +158,7 @@ export function directCompose(account, routerHistory) { ensureComposeIsVisible(getState, routerHistory); }; -}; +} export function submitCompose(routerHistory) { return function (dispatch, getState) { @@ -180,11 +183,19 @@ export function submitCompose(routerHistory) { // API call. let media_attributes; if (statusId !== null) { - media_attributes = media.map(item => ({ - id: item.get('id'), - description: item.get('description'), - focus: item.get('focus'), - })); + media_attributes = media.map(item => { + let focus; + + if (item.getIn(['meta', 'focus'])) { + focus = `${item.getIn(['meta', 'focus', 'x']).toFixed(2)},${item.getIn(['meta', 'focus', 'y']).toFixed(2)}`; + } + + return { + id: item.get('id'), + description: item.get('description'), + focus, + }; + }); } api(getState).request({ @@ -251,34 +262,34 @@ export function submitCompose(routerHistory) { dispatch(submitComposeFail(error)); }); }; -}; +} export function submitComposeRequest() { return { type: COMPOSE_SUBMIT_REQUEST, }; -}; +} export function submitComposeSuccess(status) { return { type: COMPOSE_SUBMIT_SUCCESS, status: status, }; -}; +} export function submitComposeFail(error) { return { type: COMPOSE_SUBMIT_FAIL, error: error, }; -}; +} export function doodleSet(options) { return { type: COMPOSE_DOODLE_SET, options: options, }; -}; +} export function uploadCompose(files) { return function (dispatch, getState) { @@ -341,9 +352,9 @@ export function uploadCompose(files) { } }); }).catch(error => dispatch(uploadComposeFail(error))); - }; + } }; -}; +} export const uploadComposeProcessing = () => ({ type: COMPOSE_UPLOAD_PROCESSING, @@ -399,16 +410,19 @@ export function initMediaEditModal(id) { id, }); - dispatch(openModal('FOCAL_POINT', { id })); + dispatch(openModal({ + modalType: 'FOCAL_POINT', + modalProps: { id }, + })); }; -}; +} export function onChangeMediaDescription(description) { return { type: COMPOSE_CHANGE_MEDIA_DESCRIPTION, description, }; -}; +} export function onChangeMediaFocus(focusX, focusY) { return { @@ -416,7 +430,7 @@ export function onChangeMediaFocus(focusX, focusY) { focusX, focusY, }; -}; +} export function changeUploadCompose(id, params) { return (dispatch, getState) => { @@ -427,16 +441,12 @@ export function changeUploadCompose(id, params) { // Editing already-attached media is deferred to editing the post itself. // For simplicity's sake, fake an API reply. if (media && !media.get('unattached')) { - let { description, focus } = params; - const data = media.toJS(); - - if (description) { - data.description = description; - } + const { focus, ...other } = params; + const data = { ...media.toJS(), ...other }; if (focus) { - focus = focus.split(','); - data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } }; + const [x, y] = focus.split(','); + data.meta = { focus: { x: parseFloat(x), y: parseFloat(y) } }; } dispatch(changeUploadComposeSuccess(data, true)); @@ -448,14 +458,14 @@ export function changeUploadCompose(id, params) { }); } }; -}; +} export function changeUploadComposeRequest() { return { type: COMPOSE_UPLOAD_CHANGE_REQUEST, skipLoading: true, }; -}; +} export function changeUploadComposeSuccess(media, attached) { return { @@ -464,7 +474,7 @@ export function changeUploadComposeSuccess(media, attached) { attached: attached, skipLoading: true, }; -}; +} export function changeUploadComposeFail(error) { return { @@ -472,14 +482,14 @@ export function changeUploadComposeFail(error) { error: error, skipLoading: true, }; -}; +} export function uploadComposeRequest() { return { type: COMPOSE_UPLOAD_REQUEST, skipLoading: true, }; -}; +} export function uploadComposeProgress(loaded, total) { return { @@ -487,7 +497,7 @@ export function uploadComposeProgress(loaded, total) { loaded: loaded, total: total, }; -}; +} export function uploadComposeSuccess(media, file) { return { @@ -496,7 +506,7 @@ export function uploadComposeSuccess(media, file) { file: file, skipLoading: true, }; -}; +} export function uploadComposeFail(error) { return { @@ -504,14 +514,14 @@ export function uploadComposeFail(error) { error: error, skipLoading: true, }; -}; +} export function undoUploadCompose(media_id) { return { type: COMPOSE_UPLOAD_UNDO, media_id: media_id, }; -}; +} export function clearComposeSuggestions() { if (fetchComposeSuggestionsAccountsController) { @@ -520,7 +530,7 @@ export function clearComposeSuggestions() { return { type: COMPOSE_SUGGESTIONS_CLEAR, }; -}; +} const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => { if (fetchComposeSuggestionsAccountsController) { @@ -597,7 +607,7 @@ export function fetchComposeSuggestions(token) { break; } }; -}; +} export function readyComposeSuggestionsEmojis(token, emojis) { return { @@ -605,7 +615,7 @@ export function readyComposeSuggestionsEmojis(token, emojis) { token, emojis, }; -}; +} export function readyComposeSuggestionsAccounts(token, accounts) { return { @@ -613,7 +623,7 @@ export function readyComposeSuggestionsAccounts(token, accounts) { token, accounts, }; -}; +} export const readyComposeSuggestionsTags = (token, tags) => ({ type: COMPOSE_SUGGESTIONS_READY, @@ -653,7 +663,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) { }); } }; -}; +} export function updateSuggestionTags(token) { return { @@ -701,13 +711,13 @@ export function mountCompose() { return { type: COMPOSE_MOUNT, }; -}; +} export function unmountCompose() { return { type: COMPOSE_UNMOUNT, }; -}; +} export function changeComposeAdvancedOption(option, value) { return { @@ -721,7 +731,7 @@ export function changeComposeSensitivity() { return { type: COMPOSE_SENSITIVITY_CHANGE, }; -}; +} export const changeComposeLanguage = language => ({ type: COMPOSE_LANGUAGE_CHANGE, @@ -732,28 +742,28 @@ export function changeComposeSpoilerness() { return { type: COMPOSE_SPOILERNESS_CHANGE, }; -}; +} export function changeComposeSpoilerText(text) { return { type: COMPOSE_SPOILER_TEXT_CHANGE, text, }; -}; +} export function changeComposeVisibility(value) { return { type: COMPOSE_VISIBILITY_CHANGE, value, }; -}; +} export function changeComposeContentType(value) { return { type: COMPOSE_CONTENT_TYPE_CHANGE, value, }; -}; +} export function insertEmojiCompose(position, emoji) { return { @@ -761,26 +771,26 @@ export function insertEmojiCompose(position, emoji) { position, emoji, }; -}; +} export function addPoll() { return { type: COMPOSE_POLL_ADD, }; -}; +} export function removePoll() { return { type: COMPOSE_POLL_REMOVE, }; -}; +} export function addPollOption(title) { return { type: COMPOSE_POLL_OPTION_ADD, title, }; -}; +} export function changePollOption(index, title) { return { @@ -788,14 +798,14 @@ export function changePollOption(index, title) { index, title, }; -}; +} export function removePollOption(index) { return { type: COMPOSE_POLL_OPTION_REMOVE, index, }; -}; +} export function changePollSettings(expiresIn, isMultiple) { return { @@ -803,4 +813,4 @@ export function changePollSettings(expiresIn, isMultiple) { expiresIn, isMultiple, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/conversations.js b/app/javascript/flavours/glitch/actions/conversations.js index 4ef654b1f9..8c4c4529fb 100644 --- a/app/javascript/flavours/glitch/actions/conversations.js +++ b/app/javascript/flavours/glitch/actions/conversations.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedAccounts, importFetchedStatuses, diff --git a/app/javascript/flavours/glitch/actions/custom_emojis.js b/app/javascript/flavours/glitch/actions/custom_emojis.js index 7b7d0091b5..9ec8156b17 100644 --- a/app/javascript/flavours/glitch/actions/custom_emojis.js +++ b/app/javascript/flavours/glitch/actions/custom_emojis.js @@ -14,14 +14,14 @@ export function fetchCustomEmojis() { dispatch(fetchCustomEmojisFail(error)); }); }; -}; +} export function fetchCustomEmojisRequest() { return { type: CUSTOM_EMOJIS_FETCH_REQUEST, skipLoading: true, }; -}; +} export function fetchCustomEmojisSuccess(custom_emojis) { return { @@ -29,7 +29,7 @@ export function fetchCustomEmojisSuccess(custom_emojis) { custom_emojis, skipLoading: true, }; -}; +} export function fetchCustomEmojisFail(error) { return { @@ -37,4 +37,4 @@ export function fetchCustomEmojisFail(error) { error, skipLoading: true, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/directory.js b/app/javascript/flavours/glitch/actions/directory.js index 4b2b6dd56d..cda63f2b5a 100644 --- a/app/javascript/flavours/glitch/actions/directory.js +++ b/app/javascript/flavours/glitch/actions/directory.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; diff --git a/app/javascript/flavours/glitch/actions/domain_blocks.js b/app/javascript/flavours/glitch/actions/domain_blocks.js index 34a33a6546..d06de20a2d 100644 --- a/app/javascript/flavours/glitch/actions/domain_blocks.js +++ b/app/javascript/flavours/glitch/actions/domain_blocks.js @@ -29,14 +29,14 @@ export function blockDomain(domain) { dispatch(blockDomainFail(domain, err)); }); }; -}; +} export function blockDomainRequest(domain) { return { type: DOMAIN_BLOCK_REQUEST, domain, }; -}; +} export function blockDomainSuccess(domain, accounts) { return { @@ -44,7 +44,7 @@ export function blockDomainSuccess(domain, accounts) { domain, accounts, }; -}; +} export function blockDomainFail(domain, error) { return { @@ -52,7 +52,7 @@ export function blockDomainFail(domain, error) { domain, error, }; -}; +} export function unblockDomain(domain) { return (dispatch, getState) => { @@ -66,14 +66,14 @@ export function unblockDomain(domain) { dispatch(unblockDomainFail(domain, err)); }); }; -}; +} export function unblockDomainRequest(domain) { return { type: DOMAIN_UNBLOCK_REQUEST, domain, }; -}; +} export function unblockDomainSuccess(domain, accounts) { return { @@ -81,7 +81,7 @@ export function unblockDomainSuccess(domain, accounts) { domain, accounts, }; -}; +} export function unblockDomainFail(domain, error) { return { @@ -89,7 +89,7 @@ export function unblockDomainFail(domain, error) { domain, error, }; -}; +} export function fetchDomainBlocks() { return (dispatch, getState) => { @@ -102,13 +102,13 @@ export function fetchDomainBlocks() { dispatch(fetchDomainBlocksFail(err)); }); }; -}; +} export function fetchDomainBlocksRequest() { return { type: DOMAIN_BLOCKS_FETCH_REQUEST, }; -}; +} export function fetchDomainBlocksSuccess(domains, next) { return { @@ -116,14 +116,14 @@ export function fetchDomainBlocksSuccess(domains, next) { domains, next, }; -}; +} export function fetchDomainBlocksFail(error) { return { type: DOMAIN_BLOCKS_FETCH_FAIL, error, }; -}; +} export function expandDomainBlocks() { return (dispatch, getState) => { @@ -142,13 +142,13 @@ export function expandDomainBlocks() { dispatch(expandDomainBlocksFail(err)); }); }; -}; +} export function expandDomainBlocksRequest() { return { type: DOMAIN_BLOCKS_EXPAND_REQUEST, }; -}; +} export function expandDomainBlocksSuccess(domains, next) { return { @@ -156,11 +156,11 @@ export function expandDomainBlocksSuccess(domains, next) { domains, next, }; -}; +} export function expandDomainBlocksFail(error) { return { type: DOMAIN_BLOCKS_EXPAND_FAIL, error, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/emojis.js b/app/javascript/flavours/glitch/actions/emojis.js index 7cd9d4b7b3..3b5d53996c 100644 --- a/app/javascript/flavours/glitch/actions/emojis.js +++ b/app/javascript/flavours/glitch/actions/emojis.js @@ -11,4 +11,4 @@ export function useEmoji(emoji) { dispatch(saveSettings()); }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js index 9448b1efe7..2d4d4e6206 100644 --- a/app/javascript/flavours/glitch/actions/favourites.js +++ b/app/javascript/flavours/glitch/actions/favourites.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; @@ -25,14 +26,14 @@ export function fetchFavouritedStatuses() { dispatch(fetchFavouritedStatusesFail(error)); }); }; -}; +} export function fetchFavouritedStatusesRequest() { return { type: FAVOURITED_STATUSES_FETCH_REQUEST, skipLoading: true, }; -}; +} export function fetchFavouritedStatusesSuccess(statuses, next) { return { @@ -41,7 +42,7 @@ export function fetchFavouritedStatusesSuccess(statuses, next) { next, skipLoading: true, }; -}; +} export function fetchFavouritedStatusesFail(error) { return { @@ -49,7 +50,7 @@ export function fetchFavouritedStatusesFail(error) { error, skipLoading: true, }; -}; +} export function expandFavouritedStatuses() { return (dispatch, getState) => { @@ -69,13 +70,13 @@ export function expandFavouritedStatuses() { dispatch(expandFavouritedStatusesFail(error)); }); }; -}; +} export function expandFavouritedStatusesRequest() { return { type: FAVOURITED_STATUSES_EXPAND_REQUEST, }; -}; +} export function expandFavouritedStatusesSuccess(statuses, next) { return { @@ -83,11 +84,11 @@ export function expandFavouritedStatusesSuccess(statuses, next) { statuses, next, }; -}; +} export function expandFavouritedStatusesFail(error) { return { type: FAVOURITED_STATUSES_EXPAND_FAIL, error, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/filters.js b/app/javascript/flavours/glitch/actions/filters.js index e9c609fc87..a11956ac56 100644 --- a/app/javascript/flavours/glitch/actions/filters.js +++ b/app/javascript/flavours/glitch/actions/filters.js @@ -1,4 +1,5 @@ import api from '../api'; + import { openModal } from './modal'; export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; @@ -14,9 +15,12 @@ export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS'; export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL'; export const initAddFilter = (status, { contextType }) => dispatch => - dispatch(openModal('FILTER', { - statusId: status?.get('id'), - contextType: contextType, + dispatch(openModal({ + modalType: 'FILTER', + modalProps: { + statusId: status?.get('id'), + contextType: contextType, + }, })); export const fetchFilters = () => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/height_cache.js b/app/javascript/flavours/glitch/actions/height_cache.js index 4c752993fe..a8645410c8 100644 --- a/app/javascript/flavours/glitch/actions/height_cache.js +++ b/app/javascript/flavours/glitch/actions/height_cache.js @@ -8,10 +8,10 @@ export function setHeight (key, id, height) { id, height, }; -}; +} export function clearHeight () { return { type: HEIGHT_CACHE_CLEAR, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/history.js b/app/javascript/flavours/glitch/actions/history.js index c142aaf617..52401b7dce 100644 --- a/app/javascript/flavours/glitch/actions/history.js +++ b/app/javascript/flavours/glitch/actions/history.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedAccounts } from './importer'; export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 1c9f524e43..540e6cba78 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -1,11 +1,12 @@ import escapeTextContentForBrowser from 'escape-html'; + import emojify from 'flavours/glitch/features/emoji/emoji'; -import { unescapeHTML } from 'flavours/glitch/utils/html'; import { autoHideCW } from 'flavours/glitch/utils/content_warning'; +import { unescapeHTML } from 'flavours/glitch/utils/html'; const domParser = new DOMParser(); -const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { +const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { obj[`:${emoji.shortcode}:`] = emoji; return obj; }, {}); @@ -19,7 +20,7 @@ export function searchTextFromRawStatus (status) { export function normalizeAccount(account) { account = { ...account }; - const emojiMap = makeEmojiMap(account); + const emojiMap = makeEmojiMap(account.emojis); const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); @@ -77,7 +78,7 @@ export function normalizeStatus(status, normalOldStatus, settings) { } else { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(normalStatus); + const emojiMap = makeEmojiMap(normalStatus.emojis); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); @@ -88,22 +89,48 @@ export function normalizeStatus(status, normalOldStatus, settings) { return normalStatus; } +export function normalizeStatusTranslation(translation, status) { + const emojiMap = makeEmojiMap(status.get('emojis').toJS()); + + const normalTranslation = { + detected_source_language: translation.detected_source_language, + language: translation.language, + provider: translation.provider, + contentHtml: emojify(translation.content, emojiMap), + spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap), + spoiler_text: translation.spoiler_text, + }; + + return normalTranslation; +} + export function normalizePoll(poll) { const normalPoll = { ...poll }; - const emojiMap = makeEmojiMap(normalPoll); + const emojiMap = makeEmojiMap(poll.emojis); normalPoll.options = poll.options.map((option, index) => ({ ...option, voted: poll.own_votes && poll.own_votes.includes(index), - title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); return normalPoll; } +export function normalizePollOptionTranslation(translation, poll) { + const emojiMap = makeEmojiMap(poll.get('emojis').toJS()); + + const normalTranslation = { + ...translation, + titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap), + }; + + return normalTranslation; +} + export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap(normalAnnouncement); + const emojiMap = makeEmojiMap(normalAnnouncement.emojis); normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js index 225ee7eb2a..6b8864a039 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedAccounts, importFetchedStatus } from './importer'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; @@ -54,7 +55,7 @@ export function reblog(status, visibility) { dispatch(reblogFail(status, error)); }); }; -}; +} export function unreblog(status) { return (dispatch, getState) => { @@ -67,21 +68,21 @@ export function unreblog(status) { dispatch(unreblogFail(status, error)); }); }; -}; +} export function reblogRequest(status) { return { type: REBLOG_REQUEST, status: status, }; -}; +} export function reblogSuccess(status) { return { type: REBLOG_SUCCESS, status: status, }; -}; +} export function reblogFail(status, error) { return { @@ -89,21 +90,21 @@ export function reblogFail(status, error) { status: status, error: error, }; -}; +} export function unreblogRequest(status) { return { type: UNREBLOG_REQUEST, status: status, }; -}; +} export function unreblogSuccess(status) { return { type: UNREBLOG_SUCCESS, status: status, }; -}; +} export function unreblogFail(status, error) { return { @@ -111,7 +112,7 @@ export function unreblogFail(status, error) { status: status, error: error, }; -}; +} export function favourite(status) { return function (dispatch, getState) { @@ -124,7 +125,7 @@ export function favourite(status) { dispatch(favouriteFail(status, error)); }); }; -}; +} export function unfavourite(status) { return (dispatch, getState) => { @@ -137,21 +138,21 @@ export function unfavourite(status) { dispatch(unfavouriteFail(status, error)); }); }; -}; +} export function favouriteRequest(status) { return { type: FAVOURITE_REQUEST, status: status, }; -}; +} export function favouriteSuccess(status) { return { type: FAVOURITE_SUCCESS, status: status, }; -}; +} export function favouriteFail(status, error) { return { @@ -159,21 +160,21 @@ export function favouriteFail(status, error) { status: status, error: error, }; -}; +} export function unfavouriteRequest(status) { return { type: UNFAVOURITE_REQUEST, status: status, }; -}; +} export function unfavouriteSuccess(status) { return { type: UNFAVOURITE_SUCCESS, status: status, }; -}; +} export function unfavouriteFail(status, error) { return { @@ -181,7 +182,7 @@ export function unfavouriteFail(status, error) { status: status, error: error, }; -}; +} export function bookmark(status) { return function (dispatch, getState) { @@ -194,7 +195,7 @@ export function bookmark(status) { dispatch(bookmarkFail(status, error)); }); }; -}; +} export function unbookmark(status) { return (dispatch, getState) => { @@ -207,21 +208,21 @@ export function unbookmark(status) { dispatch(unbookmarkFail(status, error)); }); }; -}; +} export function bookmarkRequest(status) { return { type: BOOKMARK_REQUEST, status: status, }; -}; +} export function bookmarkSuccess(status) { return { type: BOOKMARK_SUCCESS, status: status, }; -}; +} export function bookmarkFail(status, error) { return { @@ -229,21 +230,21 @@ export function bookmarkFail(status, error) { status: status, error: error, }; -}; +} export function unbookmarkRequest(status) { return { type: UNBOOKMARK_REQUEST, status: status, }; -}; +} export function unbookmarkSuccess(status) { return { type: UNBOOKMARK_SUCCESS, status: status, }; -}; +} export function unbookmarkFail(status, error) { return { @@ -251,7 +252,7 @@ export function unbookmarkFail(status, error) { status: status, error: error, }; -}; +} export function fetchReblogs(id) { return (dispatch, getState) => { @@ -264,14 +265,14 @@ export function fetchReblogs(id) { dispatch(fetchReblogsFail(id, error)); }); }; -}; +} export function fetchReblogsRequest(id) { return { type: REBLOGS_FETCH_REQUEST, id, }; -}; +} export function fetchReblogsSuccess(id, accounts) { return { @@ -279,14 +280,14 @@ export function fetchReblogsSuccess(id, accounts) { id, accounts, }; -}; +} export function fetchReblogsFail(id, error) { return { type: REBLOGS_FETCH_FAIL, error, }; -}; +} export function fetchFavourites(id) { return (dispatch, getState) => { @@ -299,14 +300,14 @@ export function fetchFavourites(id) { dispatch(fetchFavouritesFail(id, error)); }); }; -}; +} export function fetchFavouritesRequest(id) { return { type: FAVOURITES_FETCH_REQUEST, id, }; -}; +} export function fetchFavouritesSuccess(id, accounts) { return { @@ -314,14 +315,14 @@ export function fetchFavouritesSuccess(id, accounts) { id, accounts, }; -}; +} export function fetchFavouritesFail(id, error) { return { type: FAVOURITES_FETCH_FAIL, error, }; -}; +} export function pin(status) { return (dispatch, getState) => { @@ -334,21 +335,21 @@ export function pin(status) { dispatch(pinFail(status, error)); }); }; -}; +} export function pinRequest(status) { return { type: PIN_REQUEST, status, }; -}; +} export function pinSuccess(status) { return { type: PIN_SUCCESS, status, }; -}; +} export function pinFail(status, error) { return { @@ -356,7 +357,7 @@ export function pinFail(status, error) { status, error, }; -}; +} export function unpin (status) { return (dispatch, getState) => { @@ -369,21 +370,21 @@ export function unpin (status) { dispatch(unpinFail(status, error)); }); }; -}; +} export function unpinRequest(status) { return { type: UNPIN_REQUEST, status, }; -}; +} export function unpinSuccess(status) { return { type: UNPIN_SUCCESS, status, }; -}; +} export function unpinFail(status, error) { return { @@ -391,4 +392,4 @@ export function unpinFail(status, error) { status, error, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index 5ab9224363..b0789cd426 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { showAlertForError } from './alerts'; +import { importFetchedAccounts } from './importer'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; @@ -150,10 +151,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { dispatch(updateListSuccess(data)); if (shouldReset) { diff --git a/app/javascript/flavours/glitch/actions/local_settings.js b/app/javascript/flavours/glitch/actions/local_settings.js index a4a928611d..f2878daa50 100644 --- a/app/javascript/flavours/glitch/actions/local_settings.js +++ b/app/javascript/flavours/glitch/actions/local_settings.js @@ -1,4 +1,5 @@ import { expandSpoilers, disableSwiping } from 'flavours/glitch/initial_state'; + import { openModal } from './modal'; export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; @@ -27,20 +28,23 @@ export function checkDeprecatedLocalSettings() { } if (changed_settings.length > 0) { - dispatch(openModal('DEPRECATED_SETTINGS', { - settings: changed_settings, - onConfirm: () => dispatch(clearDeprecatedLocalSettings()), + dispatch(openModal({ + modalType: 'DEPRECATED_SETTINGS', + modalProps: { + settings: changed_settings, + onConfirm: () => dispatch(clearDeprecatedLocalSettings()), + }, })); } }; -}; +} export function clearDeprecatedLocalSettings() { return (dispatch) => { dispatch(deleteLocalSetting(['content_warnings', 'auto_unfold'])); dispatch(deleteLocalSetting(['swipe_to_change_columns'])); }; -}; +} export function changeLocalSetting(key, value) { return dispatch => { @@ -52,7 +56,7 @@ export function changeLocalSetting(key, value) { dispatch(saveLocalSettings()); }; -}; +} export function deleteLocalSetting(key) { return dispatch => { @@ -63,7 +67,7 @@ export function deleteLocalSetting(key) { dispatch(saveLocalSettings()); }; -}; +} // __TODO :__ // Right now `saveLocalSettings()` doesn't keep track of which user @@ -74,4 +78,4 @@ export function saveLocalSettings() { const localSettings = getState().get('local_settings').toJS(); localStorage.setItem('mastodon-settings', JSON.stringify(localSettings)); }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index 3b6a76bc43..ccb1b23d6f 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,8 +1,10 @@ -import api from '../api'; -import { debounce } from 'lodash'; -import compareId from '../compare_id'; import { List as ImmutableList } from 'immutable'; +import { debounce } from 'lodash'; + +import api from '../api'; +import { compareId } from '../compare_id'; + export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL'; @@ -55,7 +57,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => { client.open('POST', '/api/v1/markers', false); client.setRequestHeader('Content-Type', 'application/json'); client.setRequestHeader('Authorization', `Bearer ${accessToken}`); - client.SUBMIT(JSON.stringify(params)); + client.send(JSON.stringify(params)); } catch (e) { // Do not make the BeforeUnload handler error out } @@ -101,7 +103,7 @@ export function submitMarkersSuccess({ home, notifications }) { home: (home || {}).last_read_id, notifications: (notifications || {}).last_read_id, }; -}; +} export function submitMarkers(params = {}) { const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); @@ -111,7 +113,7 @@ export function submitMarkers(params = {}) { } return result; -}; +} export const fetchMarkers = () => (dispatch, getState) => { const params = { timeline: ['notifications'] }; @@ -130,7 +132,7 @@ export function fetchMarkersRequest() { type: MARKERS_FETCH_REQUEST, skipLoading: true, }; -}; +} export function fetchMarkersSuccess(markers) { return { @@ -138,7 +140,7 @@ export function fetchMarkersSuccess(markers) { markers, skipLoading: true, }; -}; +} export function fetchMarkersFail(error) { return { @@ -147,4 +149,4 @@ export function fetchMarkersFail(error) { skipLoading: true, skipAlert: true, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js deleted file mode 100644 index 3e576fab8e..0000000000 --- a/app/javascript/flavours/glitch/actions/modal.js +++ /dev/null @@ -1,18 +0,0 @@ -export const MODAL_OPEN = 'MODAL_OPEN'; -export const MODAL_CLOSE = 'MODAL_CLOSE'; - -export function openModal(type, props) { - return { - type: MODAL_OPEN, - modalType: type, - modalProps: props, - }; -}; - -export function closeModal(type, options = { ignoreFocus: false }) { - return { - type: MODAL_CLOSE, - modalType: type, - ignoreFocus: options.ignoreFocus, - }; -}; diff --git a/app/javascript/flavours/glitch/actions/modal.ts b/app/javascript/flavours/glitch/actions/modal.ts new file mode 100644 index 0000000000..af34f5d6af --- /dev/null +++ b/app/javascript/flavours/glitch/actions/modal.ts @@ -0,0 +1,17 @@ +import { createAction } from '@reduxjs/toolkit'; + +import type { MODAL_COMPONENTS } from '../features/ui/components/modal_root'; + +export type ModalType = keyof typeof MODAL_COMPONENTS; + +interface OpenModalPayload { + modalType: ModalType; + modalProps: unknown; +} +export const openModal = createAction('MODAL_OPEN'); + +interface CloseModalPayload { + modalType: ModalType | undefined; + ignoreFocus: boolean; +} +export const closeModal = createAction('MODAL_CLOSE'); diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js index 1ccf9592f7..4af927d932 100644 --- a/app/javascript/flavours/glitch/actions/mutes.js +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -1,7 +1,9 @@ +import { openModal } from 'flavours/glitch/actions/modal'; + import api, { getLinks } from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; -import { openModal } from 'flavours/glitch/actions/modal'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; @@ -26,13 +28,13 @@ export function fetchMutes() { dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => dispatch(fetchMutesFail(error))); }; -}; +} export function fetchMutesRequest() { return { type: MUTES_FETCH_REQUEST, }; -}; +} export function fetchMutesSuccess(accounts, next) { return { @@ -40,14 +42,14 @@ export function fetchMutesSuccess(accounts, next) { accounts, next, }; -}; +} export function fetchMutesFail(error) { return { type: MUTES_FETCH_FAIL, error, }; -}; +} export function expandMutes() { return (dispatch, getState) => { @@ -66,13 +68,13 @@ export function expandMutes() { dispatch(fetchRelationships(response.data.map(item => item.id))); }).catch(error => dispatch(expandMutesFail(error))); }; -}; +} export function expandMutesRequest() { return { type: MUTES_EXPAND_REQUEST, }; -}; +} export function expandMutesSuccess(accounts, next) { return { @@ -80,14 +82,14 @@ export function expandMutesSuccess(accounts, next) { accounts, next, }; -}; +} export function expandMutesFail(error) { return { type: MUTES_EXPAND_FAIL, error, }; -}; +} export function initMuteModal(account) { return dispatch => { @@ -96,7 +98,7 @@ export function initMuteModal(account) { account, }); - dispatch(openModal('MUTE')); + dispatch(openModal({ modalType: 'MUTE' })); }; } diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 158a5b7e43..a80746b756 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -1,5 +1,15 @@ +import { IntlMessageFormat } from 'intl-messageformat'; +import { defineMessages } from 'react-intl'; + +import { List as ImmutableList } from 'immutable'; + +import { compareId } from 'flavours/glitch/compare_id'; +import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; +import { unescapeHTML } from 'flavours/glitch/utils/html'; +import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; + import api, { getLinks } from '../api'; -import IntlMessageFormat from 'intl-messageformat'; + import { fetchFollowRequests, fetchRelationships } from './accounts'; import { importFetchedAccount, @@ -9,12 +19,9 @@ import { } from './importer'; import { submitMarkers } from './markers'; import { saveSettings } from './settings'; -import { defineMessages } from 'react-intl'; -import { List as ImmutableList } from 'immutable'; -import { unescapeHTML } from 'flavours/glitch/utils/html'; -import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; -import compareId from 'flavours/glitch/compare_id'; -import { requestNotificationPermission } from 'flavours/glitch/utils/notifications'; + + + export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -129,7 +136,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { }); } }; -}; +} const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); @@ -209,14 +216,14 @@ export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) { done(); }); }; -}; +} export function expandNotificationsRequest(isLoadingMore) { return { type: NOTIFICATIONS_EXPAND_REQUEST, skipLoading: !isLoadingMore, }; -}; +} export function expandNotificationsSuccess(notifications, next, isLoadingMore, isLoadingRecent, usePendingItems) { return { @@ -227,7 +234,7 @@ export function expandNotificationsSuccess(notifications, next, isLoadingMore, i usePendingItems, skipLoading: !isLoadingMore, }; -}; +} export function expandNotificationsFail(error, isLoadingMore) { return { @@ -236,7 +243,7 @@ export function expandNotificationsFail(error, isLoadingMore) { skipLoading: !isLoadingMore, skipAlert: !isLoadingMore || error.name === 'AbortError', }; -}; +} export function clearNotifications() { return (dispatch, getState) => { @@ -246,14 +253,14 @@ export function clearNotifications() { api(getState).post('/api/v1/notifications/clear'); }; -}; +} export function scrollTopNotifications(top) { return { type: NOTIFICATIONS_SCROLL_TOP, top, }; -}; +} export function deleteMarkedNotifications() { return (dispatch, getState) => { @@ -277,33 +284,33 @@ export function deleteMarkedNotifications() { dispatch(deleteMarkedNotificationsFail(error)); }); }; -}; +} export function enterNotificationClearingMode(yes) { return { type: NOTIFICATIONS_ENTER_CLEARING_MODE, yes: yes, }; -}; +} export function markAllNotifications(yes) { return { type: NOTIFICATIONS_MARK_ALL_FOR_DELETE, yes: yes, // true, false or null. null = invert }; -}; +} export function deleteMarkedNotificationsRequest() { return { type: NOTIFICATIONS_DELETE_MARKED_REQUEST, }; -}; +} export function deleteMarkedNotificationsFail() { return { type: NOTIFICATIONS_DELETE_MARKED_FAIL, }; -}; +} export function markNotificationForDelete(id, yes) { return { @@ -311,32 +318,32 @@ export function markNotificationForDelete(id, yes) { id: id, yes: yes, }; -}; +} export function deleteMarkedNotificationsSuccess() { return { type: NOTIFICATIONS_DELETE_MARKED_SUCCESS, }; -}; +} export function mountNotifications() { return { type: NOTIFICATIONS_MOUNT, }; -}; +} export function unmountNotifications() { return { type: NOTIFICATIONS_UNMOUNT, }; -}; +} export function notificationsSetVisibility(visibility) { return { type: NOTIFICATIONS_SET_VISIBILITY, visibility: visibility, }; -}; +} export function setFilter (filterType) { return dispatch => { @@ -348,13 +355,13 @@ export function setFilter (filterType) { dispatch(expandNotifications({ forceLoad: true })); dispatch(saveSettings()); }; -}; +} export function markNotificationsAsRead() { return { type: NOTIFICATIONS_MARK_AS_READ, }; -}; +} // Browser support export function setupBrowserNotifications() { @@ -379,7 +386,7 @@ export function requestBrowserPermission(callback = noOp) { callback(permission); }); }; -}; +} export function setBrowserSupport (value) { return { diff --git a/app/javascript/flavours/glitch/actions/onboarding.js b/app/javascript/flavours/glitch/actions/onboarding.js index a161c50efe..a4a525c427 100644 --- a/app/javascript/flavours/glitch/actions/onboarding.js +++ b/app/javascript/flavours/glitch/actions/onboarding.js @@ -6,9 +6,11 @@ export function showOnboardingOnce() { const alreadySeen = getState().getIn(['settings', 'onboarded']); if (!alreadySeen) { - dispatch(openModal('ONBOARDING')); + dispatch(openModal({ + modalType: 'ONBOARDING', + })); dispatch(changeSetting(['onboarded'], true)); dispatch(saveSettings()); } }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/picture_in_picture.js b/app/javascript/flavours/glitch/actions/picture_in_picture.js index 33d8d57d47..898375abeb 100644 --- a/app/javascript/flavours/glitch/actions/picture_in_picture.js +++ b/app/javascript/flavours/glitch/actions/picture_in_picture.js @@ -20,9 +20,10 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; * @param {string} accountId * @param {string} playerType * @param {MediaProps} props - * @return {object} + * @returns {object} */ export const deployPictureInPicture = (statusId, accountId, playerType, props) => { + // @ts-expect-error return (dispatch, getState) => { // Do not open a player for a toot that does not exist if (getState().hasIn(['statuses', statusId])) { diff --git a/app/javascript/flavours/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js index 0926978ac8..8aca199e97 100644 --- a/app/javascript/flavours/glitch/actions/pin_statuses.js +++ b/app/javascript/flavours/glitch/actions/pin_statuses.js @@ -1,12 +1,14 @@ +import { me } from 'flavours/glitch/initial_state'; + import api from '../api'; + import { importFetchedStatuses } from './importer'; + export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; -import { me } from 'flavours/glitch/initial_state'; - export function fetchPinnedStatuses() { return (dispatch, getState) => { dispatch(fetchPinnedStatusesRequest()); @@ -18,13 +20,13 @@ export function fetchPinnedStatuses() { dispatch(fetchPinnedStatusesFail(error)); }); }; -}; +} export function fetchPinnedStatusesRequest() { return { type: PINNED_STATUSES_FETCH_REQUEST, }; -}; +} export function fetchPinnedStatusesSuccess(statuses, next) { return { @@ -32,11 +34,11 @@ export function fetchPinnedStatusesSuccess(statuses, next) { statuses, next, }; -}; +} export function fetchPinnedStatusesFail(error) { return { type: PINNED_STATUSES_FETCH_FAIL, error, }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/polls.js b/app/javascript/flavours/glitch/actions/polls.js index 8e8b82df5d..a37410dc90 100644 --- a/app/javascript/flavours/glitch/actions/polls.js +++ b/app/javascript/flavours/glitch/actions/polls.js @@ -1,4 +1,5 @@ import api from '../api'; + import { importFetchedPoll } from './importer'; export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/push_notifications/index.js b/app/javascript/flavours/glitch/actions/push_notifications/index.js index 9dcc4bd4bb..46b63867f1 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/index.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/index.js @@ -1,5 +1,5 @@ -import { setAlerts } from './setter'; import { saveSettings } from './registerer'; +import { setAlerts } from './setter'; export function changeAlerts(path, value) { return dispatch => { diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js index 762fe260c7..336bbc6869 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js @@ -1,12 +1,13 @@ import api from '../../api'; import { pushNotificationsSetting } from '../../settings'; + import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; // Taken from https://www.npmjs.com/package/web-push const urlBase64ToUint8Array = (base64String) => { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) - .replace(/\-/g, '+') + .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); diff --git a/app/javascript/flavours/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js index fbe5b3791b..756b8cd05e 100644 --- a/app/javascript/flavours/glitch/actions/reports.js +++ b/app/javascript/flavours/glitch/actions/reports.js @@ -1,4 +1,5 @@ import api from '../api'; + import { openModal } from './modal'; export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; @@ -6,9 +7,12 @@ export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; export const initReport = (account, status) => dispatch => - dispatch(openModal('REPORT', { - accountId: account.get('id'), - statusId: status?.get('id'), + dispatch(openModal({ + modalType: 'REPORT', + modalProps: { + accountId: account.get('id'), + statusId: status?.get('id'), + }, })); export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index f21c0058ba..d5154c6a84 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -1,4 +1,5 @@ import api from '../api'; + import { fetchRelationships } from './accounts'; import { importFetchedAccounts, importFetchedStatuses } from './importer'; @@ -19,13 +20,13 @@ export function changeSearch(value) { type: SEARCH_CHANGE, value, }; -}; +} export function clearSearch() { return { type: SEARCH_CLEAR, }; -}; +} export function submitSearch() { return (dispatch, getState) => { @@ -60,13 +61,13 @@ export function submitSearch() { dispatch(fetchSearchFail(error)); }); }; -}; +} export function fetchSearchRequest() { return { type: SEARCH_FETCH_REQUEST, }; -}; +} export function fetchSearchSuccess(results, searchTerm) { return { @@ -74,14 +75,14 @@ export function fetchSearchSuccess(results, searchTerm) { results, searchTerm, }; -}; +} export function fetchSearchFail(error) { return { type: SEARCH_FETCH_FAIL, error, }; -}; +} export const expandSearch = type => (dispatch, getState) => { const value = getState().getIn(['search', 'value']); diff --git a/app/javascript/flavours/glitch/actions/server.js b/app/javascript/flavours/glitch/actions/server.js index 31d4aea100..65f3efc3a7 100644 --- a/app/javascript/flavours/glitch/actions/server.js +++ b/app/javascript/flavours/glitch/actions/server.js @@ -1,10 +1,15 @@ import api from '../api'; + import { importFetchedAccount } from './importer'; export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS'; +export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL'; + export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; @@ -14,6 +19,10 @@ export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SU export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; export const fetchServer = () => (dispatch, getState) => { + if (getState().getIn(['server', 'server', 'isLoading'])) { + return; + } + dispatch(fetchServerRequest()); api(getState) @@ -37,7 +46,34 @@ const fetchServerFail = error => ({ error, }); +export const fetchServerTranslationLanguages = () => (dispatch, getState) => { + dispatch(fetchServerTranslationLanguagesRequest()); + + api(getState) + .get('/api/v1/instance/translation_languages').then(({ data }) => { + dispatch(fetchServerTranslationLanguagesSuccess(data)); + }).catch(err => dispatch(fetchServerTranslationLanguagesFail(err))); +}; + +const fetchServerTranslationLanguagesRequest = () => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST, +}); + +const fetchServerTranslationLanguagesSuccess = translationLanguages => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS, + translationLanguages, +}); + +const fetchServerTranslationLanguagesFail = error => ({ + type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL, + error, +}); + export const fetchExtendedDescription = () => (dispatch, getState) => { + if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) { + return; + } + dispatch(fetchExtendedDescriptionRequest()); api(getState) @@ -61,6 +97,10 @@ const fetchExtendedDescriptionFail = error => ({ }); export const fetchDomainBlocks = () => (dispatch, getState) => { + if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) { + return; + } + dispatch(fetchDomainBlocksRequest()); api(getState) diff --git a/app/javascript/flavours/glitch/actions/settings.js b/app/javascript/flavours/glitch/actions/settings.js index 5634a11efb..120ae133ed 100644 --- a/app/javascript/flavours/glitch/actions/settings.js +++ b/app/javascript/flavours/glitch/actions/settings.js @@ -1,5 +1,7 @@ -import api from '../api'; import { debounce } from 'lodash'; + +import api from '../api'; + import { showAlertForError } from './alerts'; export const SETTING_CHANGE = 'SETTING_CHANGE'; @@ -15,7 +17,7 @@ export function changeSetting(path, value) { dispatch(saveSettings()); }; -}; +} const debouncedSave = debounce((dispatch, getState) => { if (getState().getIn(['settings', 'saved'])) { @@ -31,4 +33,4 @@ const debouncedSave = debounce((dispatch, getState) => { export function saveSettings() { return (dispatch, getState) => debouncedSave(dispatch, getState); -}; +} diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index efb4cc33b5..5bdd31c343 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -1,8 +1,8 @@ import api from '../api'; -import { deleteFromTimelines } from './timelines'; -import { importFetchedStatus, importFetchedStatuses } from './importer'; import { ensureComposeIsVisible, setComposeToStatus } from './compose'; +import { importFetchedStatus, importFetchedStatuses } from './importer'; +import { deleteFromTimelines } from './timelines'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -45,7 +45,7 @@ export function fetchStatusRequest(id, skipLoading) { id, skipLoading, }; -}; +} export function fetchStatus(id, forceFetch = false) { return (dispatch, getState) => { @@ -66,14 +66,14 @@ export function fetchStatus(id, forceFetch = false) { dispatch(fetchStatusFail(id, error, skipLoading)); }); }; -}; +} export function fetchStatusSuccess(skipLoading) { return { type: STATUS_FETCH_SUCCESS, skipLoading, }; -}; +} export function fetchStatusFail(id, error, skipLoading) { return { @@ -83,7 +83,7 @@ export function fetchStatusFail(id, error, skipLoading) { skipLoading, skipAlert: true, }; -}; +} export function redraft(status, raw_text, content_type) { return { @@ -92,7 +92,7 @@ export function redraft(status, raw_text, content_type) { raw_text, content_type, }; -}; +} export const editStatus = (id, routerHistory) => (dispatch, getState) => { let status = getState().getIn(['statuses', id]); @@ -148,21 +148,21 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { dispatch(deleteStatusFail(id, error)); }); }; -}; +} export function deleteStatusRequest(id) { return { type: STATUS_DELETE_REQUEST, id: id, }; -}; +} export function deleteStatusSuccess(id) { return { type: STATUS_DELETE_SUCCESS, id: id, }; -}; +} export function deleteStatusFail(id, error) { return { @@ -170,7 +170,7 @@ export function deleteStatusFail(id, error) { id: id, error: error, }; -}; +} export const updateStatus = status => dispatch => dispatch(importFetchedStatus(status)); @@ -191,14 +191,14 @@ export function fetchContext(id) { dispatch(fetchContextFail(id, error)); }); }; -}; +} export function fetchContextRequest(id) { return { type: CONTEXT_FETCH_REQUEST, id, }; -}; +} export function fetchContextSuccess(id, ancestors, descendants) { return { @@ -208,7 +208,7 @@ export function fetchContextSuccess(id, ancestors, descendants) { descendants, statuses: ancestors.concat(descendants), }; -}; +} export function fetchContextFail(id, error) { return { @@ -217,7 +217,7 @@ export function fetchContextFail(id, error) { error, skipAlert: true, }; -}; +} export function muteStatus(id) { return (dispatch, getState) => { @@ -229,21 +229,21 @@ export function muteStatus(id) { dispatch(muteStatusFail(id, error)); }); }; -}; +} export function muteStatusRequest(id) { return { type: STATUS_MUTE_REQUEST, id, }; -}; +} export function muteStatusSuccess(id) { return { type: STATUS_MUTE_SUCCESS, id, }; -}; +} export function muteStatusFail(id, error) { return { @@ -251,7 +251,7 @@ export function muteStatusFail(id, error) { id, error, }; -}; +} export function unmuteStatus(id) { return (dispatch, getState) => { @@ -263,21 +263,21 @@ export function unmuteStatus(id) { dispatch(unmuteStatusFail(id, error)); }); }; -}; +} export function unmuteStatusRequest(id) { return { type: STATUS_UNMUTE_REQUEST, id, }; -}; +} export function unmuteStatusSuccess(id) { return { type: STATUS_UNMUTE_SUCCESS, id, }; -}; +} export function unmuteStatusFail(id, error) { return { @@ -285,7 +285,7 @@ export function unmuteStatusFail(id, error) { id, error, }; -}; +} export function hideStatus(ids) { if (!Array.isArray(ids)) { @@ -296,7 +296,7 @@ export function hideStatus(ids) { type: STATUS_HIDE, ids, }; -}; +} export function revealStatus(ids) { if (!Array.isArray(ids)) { @@ -307,7 +307,7 @@ export function revealStatus(ids) { type: STATUS_REVEAL, ids, }; -}; +} export function toggleStatusCollapse(id, isCollapsed) { return { @@ -315,7 +315,7 @@ export function toggleStatusCollapse(id, isCollapsed) { id, isCollapsed, }; -}; +} export const translateStatus = id => (dispatch, getState) => { dispatch(translateStatusRequest(id)); @@ -344,7 +344,8 @@ export const translateStatusFail = (id, error) => ({ error, }); -export const undoStatusTranslation = id => ({ +export const undoStatusTranslation = (id, pollId) => ({ type: STATUS_TRANSLATE_UNDO, id, + pollId, }); diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js index 9dbc0b2148..e57b37a122 100644 --- a/app/javascript/flavours/glitch/actions/store.js +++ b/app/javascript/flavours/glitch/actions/store.js @@ -1,4 +1,5 @@ import { Iterable, fromJS } from 'immutable'; + import { hydrateCompose } from './compose'; import { importFetchedAccounts } from './importer'; import { saveSettings } from './settings'; @@ -18,7 +19,7 @@ const applyMigrations = (state) => { if (state.getIn(['settings', 'notifications', 'showUnread']) !== false) { state.setIn(['settings', 'notifications', 'showUnread'], state.getIn(['local_settings', 'notifications', 'show_unread'])); } - state.removeIn(['local_settings', 'notifications', 'show_unread']) + state.removeIn(['local_settings', 'notifications', 'show_unread']); } }); }; @@ -36,4 +37,4 @@ export function hydrateStore(rawState) { dispatch(importFetchedAccounts(Object.values(rawState.accounts))); dispatch(saveSettings()); }; -}; +} diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index ffac1b2582..f1c44d2e29 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -1,6 +1,18 @@ // @ts-check +import { getLocale } from 'flavours/glitch/locales'; + import { connectStream } from '../stream'; + +import { + fetchAnnouncements, + updateAnnouncements, + updateReaction as updateAnnouncementsReaction, + deleteAnnouncement, +} from './announcements'; +import { updateConversations } from './conversations'; +import { updateNotifications, expandNotifications } from './notifications'; +import { updateStatus } from './statuses'; import { updateTimeline, deleteFromTimelines, @@ -12,22 +24,10 @@ import { fillCommunityTimelineGaps, fillListTimelineGaps, } from './timelines'; -import { updateNotifications, expandNotifications } from './notifications'; -import { updateConversations } from './conversations'; -import { updateStatus } from './statuses'; -import { - fetchAnnouncements, - updateAnnouncements, - updateReaction as updateAnnouncementsReaction, - deleteAnnouncement, -} from './announcements'; -import { getLocale } from 'mastodon/locales'; - -const { messages } = getLocale(); /** * @param {number} max - * @return {number} + * @returns {number} */ const randomUpTo = max => Math.floor(Math.random() * Math.floor(max)); @@ -40,19 +40,24 @@ const randomUpTo = max => * @param {function(Function, Function): void} [options.fallback] * @param {function(): void} [options.fillGaps] * @param {function(object): boolean} [options.accept] - * @return {function(): void} + * @returns {function(): void} */ -export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => - connectStream(channelName, params, (dispatch, getState) => { +export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => { + const { messages } = getLocale(); + + return connectStream(channelName, params, (dispatch, getState) => { const locale = getState().getIn(['meta', 'locale']); + // @ts-expect-error let pollingId; /** * @param {function(Function, Function): void} fallback */ + const useFallback = fallback => { fallback(dispatch, () => { + // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000)); }); }; @@ -61,6 +66,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onConnect() { dispatch(connectTimeline(timelineId)); + // @ts-expect-error if (pollingId) { clearTimeout(pollingId); pollingId = null; @@ -75,6 +81,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti dispatch(disconnectTimeline(timelineId)); if (options.fallback) { + // @ts-expect-error pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000)); } }, @@ -82,24 +89,30 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onReceive (data) { switch(data.event) { case 'update': + // @ts-expect-error dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept)); break; case 'status.update': + // @ts-expect-error dispatch(updateStatus(JSON.parse(data.payload))); break; case 'delete': dispatch(deleteFromTimelines(data.payload)); break; case 'notification': + // @ts-expect-error dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); break; case 'conversation': + // @ts-expect-error dispatch(updateConversations(JSON.parse(data.payload))); break; case 'announcement': + // @ts-expect-error dispatch(updateAnnouncements(JSON.parse(data.payload))); break; case 'announcement.reaction': + // @ts-expect-error dispatch(updateAnnouncementsReaction(JSON.parse(data.payload))); break; case 'announcement.delete': @@ -109,27 +122,31 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, }; }); +}; /** * @param {Function} dispatch * @param {function(): void} done */ const refreshHomeTimelineAndNotification = (dispatch, done) => { + // @ts-expect-error dispatch(expandHomeTimeline({}, () => + // @ts-expect-error dispatch(expandNotifications({}, () => dispatch(fetchAnnouncements(done)))))); }; /** - * @return {function(): void} + * @returns {function(): void} */ export const connectUserStream = () => + // @ts-expect-error connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); /** * @param {Object} options * @param {boolean} [options.onlyMedia] - * @return {function(): void} + * @returns {function(): void} */ export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) }); @@ -139,7 +156,7 @@ export const connectCommunityStream = ({ onlyMedia } = {}) => * @param {boolean} [options.onlyMedia] * @param {boolean} [options.onlyRemote] * @param {boolean} [options.allowLocalOnly] - * @return {function(): void} + * @returns {function(): void} */ export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) }); @@ -149,20 +166,20 @@ export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = * @param {string} tagName * @param {boolean} onlyLocal * @param {function(object): boolean} accept - * @return {function(): void} + * @returns {function(): void} */ export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) => connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept }); /** - * @return {function(): void} + * @returns {function(): void} */ export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); /** * @param {string} listId - * @return {function(): void} + * @returns {function(): void} */ export const connectListStream = listId => connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) }); diff --git a/app/javascript/flavours/glitch/actions/suggestions.js b/app/javascript/flavours/glitch/actions/suggestions.js index 1f1116e75e..870a311024 100644 --- a/app/javascript/flavours/glitch/actions/suggestions.js +++ b/app/javascript/flavours/glitch/actions/suggestions.js @@ -1,6 +1,7 @@ import api from '../api'; -import { importFetchedAccounts } from './importer'; + import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; @@ -21,14 +22,14 @@ export function fetchSuggestions(withRelationships = false) { } }).catch(error => dispatch(fetchSuggestionsFail(error))); }; -}; +} export function fetchSuggestionsRequest() { return { type: SUGGESTIONS_FETCH_REQUEST, skipLoading: true, }; -}; +} export function fetchSuggestionsSuccess(suggestions) { return { @@ -36,7 +37,7 @@ export function fetchSuggestionsSuccess(suggestions) { suggestions, skipLoading: true, }; -}; +} export function fetchSuggestionsFail(error) { return { @@ -45,7 +46,7 @@ export function fetchSuggestionsFail(error) { skipLoading: true, skipAlert: true, }; -}; +} export const dismissSuggestion = accountId => (dispatch, getState) => { dispatch({ diff --git a/app/javascript/flavours/glitch/actions/tags.js b/app/javascript/flavours/glitch/actions/tags.js index 08a08cda31..dda8c924bb 100644 --- a/app/javascript/flavours/glitch/actions/tags.js +++ b/app/javascript/flavours/glitch/actions/tags.js @@ -60,7 +60,7 @@ export function fetchFollowedHashtagsRequest() { return { type: FOLLOWED_HASHTAGS_FETCH_REQUEST, }; -}; +} export function fetchFollowedHashtagsSuccess(followed_tags, next) { return { @@ -68,14 +68,14 @@ export function fetchFollowedHashtagsSuccess(followed_tags, next) { followed_tags, next, }; -}; +} export function fetchFollowedHashtagsFail(error) { return { type: FOLLOWED_HASHTAGS_FETCH_FAIL, error, }; -}; +} export function expandFollowedHashtags() { return (dispatch, getState) => { @@ -94,13 +94,13 @@ export function expandFollowedHashtags() { dispatch(expandFollowedHashtagsFail(error)); }); }; -}; +} export function expandFollowedHashtagsRequest() { return { type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, }; -}; +} export function expandFollowedHashtagsSuccess(followed_tags, next) { return { @@ -108,14 +108,14 @@ export function expandFollowedHashtagsSuccess(followed_tags, next) { followed_tags, next, }; -}; +} export function expandFollowedHashtagsFail(error) { return { type: FOLLOWED_HASHTAGS_EXPAND_FAIL, error, }; -}; +} export const followHashtag = name => (dispatch, getState) => { dispatch(followHashtagRequest(name)); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index a1c4dd43ad..7d4d56a784 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,10 +1,12 @@ +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +import api, { getLinks } from 'flavours/glitch/api'; +import { compareId } from 'flavours/glitch/compare_id'; +import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; +import { toServerSideType } from 'flavours/glitch/utils/filters'; + import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; -import api, { getLinks } from 'flavours/glitch/api'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -import compareId from 'flavours/glitch/compare_id'; -import { me, usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; -import { toServerSideType } from 'flavours/glitch/utils/filters'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -55,14 +57,14 @@ export function updateTimeline(timeline, status, accept) { timeline, status, usePendingItems: preferPendingItems, - filtered + filtered, }); if (timeline === 'home') { dispatch(submitMarkers()); } }; -}; +} export function deleteFromTimelines(id) { return (dispatch, getState) => { @@ -78,13 +80,13 @@ export function deleteFromTimelines(id) { reblogOf, }); }; -}; +} export function clearTimeline(timeline) { return (dispatch) => { dispatch({ type: TIMELINE_CLEAR, timeline }); }; -}; +} const noOp = () => {}; @@ -121,7 +123,6 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); @@ -134,7 +135,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { done(); }); }; -}; +} export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) { return (dispatch, getState) => { @@ -163,10 +164,10 @@ export const expandListTimeline = (id, { maxId } = {}, done = noOp) = export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, - any: parseTags(tags, 'any'), - all: parseTags(tags, 'all'), - none: parseTags(tags, 'none'), - local: local, + any: parseTags(tags, 'any'), + all: parseTags(tags, 'all'), + none: parseTags(tags, 'none'), + local: local, }, done); }; @@ -181,7 +182,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) { timeline, skipLoading: !isLoadingMore, }; -}; +} export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) { return { @@ -194,7 +195,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi usePendingItems, skipLoading: !isLoadingMore, }; -}; +} export function expandTimelineFail(timeline, error, isLoadingMore) { return { @@ -204,7 +205,7 @@ export function expandTimelineFail(timeline, error, isLoadingMore) { skipLoading: !isLoadingMore, skipNotFound: timeline.startsWith('account:'), }; -}; +} export function scrollTopTimeline(timeline, top) { return { @@ -212,7 +213,7 @@ export function scrollTopTimeline(timeline, top) { timeline, top, }; -}; +} export function connectTimeline(timeline) { return { @@ -220,7 +221,7 @@ export function connectTimeline(timeline) { timeline, usePendingItems: preferPendingItems, }; -}; +} export const disconnectTimeline = timeline => ({ type: TIMELINE_DISCONNECT, diff --git a/app/javascript/flavours/glitch/actions/trends.js b/app/javascript/flavours/glitch/actions/trends.js index edda0b5b5d..d314423884 100644 --- a/app/javascript/flavours/glitch/actions/trends.js +++ b/app/javascript/flavours/glitch/actions/trends.js @@ -1,4 +1,5 @@ import api, { getLinks } from '../api'; + import { importFetchedStatuses } from './importer'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/api.js b/app/javascript/flavours/glitch/api.js index 6bbddbef66..948ffbc95c 100644 --- a/app/javascript/flavours/glitch/api.js +++ b/app/javascript/flavours/glitch/api.js @@ -2,8 +2,8 @@ import axios from 'axios'; import LinkHeader from 'http-link-header'; -import ready from './ready'; +import ready from './ready'; /** * @param {import('axios').AxiosResponse} response * @returns {LinkHeader} @@ -36,7 +36,7 @@ const setCSRFHeader = () => { ready(setCSRFHeader); /** - * @param {() => import('immutable').Map} getState + * @param {() => import('immutable').Map} getState * @returns {import('axios').RawAxiosRequestHeaders} */ const authorizationHeaderFromState = getState => { @@ -52,7 +52,7 @@ const authorizationHeaderFromState = getState => { }; /** - * @param {() => import('immutable').Map} getState + * @param {() => import('immutable').Map} getState * @returns {import('axios').AxiosInstance} */ export default function api(getState) { diff --git a/app/javascript/flavours/glitch/base_polyfills.js b/app/javascript/flavours/glitch/base_polyfills.js deleted file mode 100644 index 12096d9021..0000000000 --- a/app/javascript/flavours/glitch/base_polyfills.js +++ /dev/null @@ -1,47 +0,0 @@ -import 'intl'; -import 'intl/locale-data/jsonp/en'; -import 'es6-symbol/implement'; -import includes from 'array-includes'; -import assign from 'object-assign'; -import values from 'object.values'; -import isNaN from 'is-nan'; -import { decode as decodeBase64 } from './utils/base64'; -import promiseFinally from 'promise.prototype.finally'; - -if (!Array.prototype.includes) { - includes.shim(); -} - -if (!Object.assign) { - Object.assign = assign; -} - -if (!Object.values) { - values.shim(); -} - -if (!Number.isNaN) { - Number.isNaN = isNaN; -} - -promiseFinally.shim(); - -if (!HTMLCanvasElement.prototype.toBlob) { - const BASE64_MARKER = ';base64,'; - - Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { - value(callback, type = 'image/png', quality) { - const dataURL = this.toDataURL(type, quality); - let data; - - if (dataURL.indexOf(BASE64_MARKER) >= 0) { - const [, base64] = dataURL.split(BASE64_MARKER); - data = decodeBase64(base64); - } else { - [, data] = dataURL.split(','); - } - - callback(new Blob([data], { type })); - }, - }); -} diff --git a/app/javascript/mastodon/blurhash.js b/app/javascript/flavours/glitch/blurhash.ts similarity index 81% rename from app/javascript/mastodon/blurhash.js rename to app/javascript/flavours/glitch/blurhash.ts index 5adcc3e770..dadf2b7f2c 100644 --- a/app/javascript/mastodon/blurhash.js +++ b/app/javascript/flavours/glitch/blurhash.ts @@ -84,7 +84,7 @@ const DIGIT_CHARACTERS = [ '~', ]; -export const decode83 = (str) => { +export const decode83 = (str: string) => { let value = 0; let c, digit; @@ -97,13 +97,13 @@ export const decode83 = (str) => { return value; }; -export const intToRGB = int => ({ - r: Math.max(0, (int >> 16)), +export const intToRGB = (int: number) => ({ + r: Math.max(0, int >> 16), g: Math.max(0, (int >> 8) & 255), - b: Math.max(0, (int & 255)), + b: Math.max(0, int & 255), }); -export const getAverageFromBlurhash = blurhash => { +export const getAverageFromBlurhash = (blurhash: string) => { if (!blurhash) { return null; } diff --git a/app/javascript/mastodon/compare_id.js b/app/javascript/flavours/glitch/compare_id.ts similarity index 75% rename from app/javascript/mastodon/compare_id.js rename to app/javascript/flavours/glitch/compare_id.ts index d2bd74f447..30b0572481 100644 --- a/app/javascript/mastodon/compare_id.js +++ b/app/javascript/flavours/glitch/compare_id.ts @@ -1,4 +1,4 @@ -export default function compareId (id1, id2) { +export function compareId(id1: string, id2: string) { if (id1 === id2) { return 0; } diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.jsx similarity index 93% rename from app/javascript/flavours/glitch/components/account.js rename to app/javascript/flavours/glitch/components/account.jsx index 8e810ce5fb..518464b040 100644 --- a/app/javascript/flavours/glitch/components/account.js +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -1,20 +1,24 @@ -import React, { Fragment } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import Avatar from './avatar'; -import DisplayName from './display_name'; -import Permalink from './permalink'; -import IconButton from './icon_button'; + import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Skeleton } from 'flavours/glitch/components/skeleton'; import { me } from 'flavours/glitch/initial_state'; -import RelativeTimestamp from './relative_timestamp'; -import Skeleton from 'flavours/glitch/components/skeleton'; + +import { Avatar } from './avatar'; +import { DisplayName } from './display_name'; +import { IconButton } from './icon_button'; +import Permalink from './permalink'; +import { RelativeTimestamp } from './relative_timestamp'; + const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, @@ -23,7 +27,6 @@ const messages = defineMessages({ block: { id: 'account.block', defaultMessage: 'Block @{name}' }, }); -export default @injectIntl class Account extends ImmutablePureComponent { static propTypes = { @@ -48,27 +51,27 @@ class Account extends ImmutablePureComponent { handleFollow = () => { this.props.onFollow(this.props.account); - } + }; handleBlock = () => { this.props.onBlock(this.props.account); - } + }; handleMute = () => { this.props.onMute(this.props.account); - } + }; handleMuteNotifications = () => { this.props.onMuteNotifications(this.props.account, true); - } + }; handleUnmuteNotifications = () => { this.props.onMuteNotifications(this.props.account, false); - } + }; handleAction = () => { this.props.onActionClick(this.props.account); - } + }; render () { const { @@ -98,10 +101,10 @@ class Account extends ImmutablePureComponent { if (hidden) { return ( - + <> {account.get('display_name')} {account.get('username')} - + ); } @@ -129,10 +132,10 @@ class Account extends ImmutablePureComponent { hidingNotificationsButton = ; } buttons = ( - + <> {hidingNotificationsButton} - + ); } else if (defaultAction === 'mute') { buttons = ; @@ -184,3 +187,5 @@ class Account extends ImmutablePureComponent { } } + +export default injectIntl(Account); diff --git a/app/javascript/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.jsx similarity index 90% rename from app/javascript/flavours/glitch/components/admin/Counter.js rename to app/javascript/flavours/glitch/components/admin/Counter.jsx index 5b6a19f8da..9bb792fc9d 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.js +++ b/app/javascript/flavours/glitch/components/admin/Counter.jsx @@ -1,10 +1,14 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; +import { PureComponent } from 'react'; + import { FormattedNumber } from 'react-intl'; -import { Sparklines, SparklinesCurve } from 'react-sparklines'; + import classNames from 'classnames'; -import Skeleton from 'flavours/glitch/components/skeleton'; + +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; const percIncrease = (a, b) => { let percent; @@ -24,7 +28,7 @@ const percIncrease = (a, b) => { return percent; }; -export default class Counter extends React.PureComponent { +export default class Counter extends PureComponent { static propTypes = { measure: PropTypes.string.isRequired, @@ -62,25 +66,25 @@ export default class Counter extends React.PureComponent { if (loading) { content = ( - + <> - + ); } else { const measure = data[0]; const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1); content = ( - + <> {measure.human_value || } {measure.previous_total && ( 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'})} - + ); } const inner = ( - + <>

{content}
@@ -96,7 +100,7 @@ export default class Counter extends React.PureComponent { )} - + ); if (href) { diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.js b/app/javascript/flavours/glitch/components/admin/Dimension.jsx similarity index 93% rename from app/javascript/flavours/glitch/components/admin/Dimension.js rename to app/javascript/flavours/glitch/components/admin/Dimension.jsx index 3dac8c6c24..793fe2dd76 100644 --- a/app/javascript/flavours/glitch/components/admin/Dimension.js +++ b/app/javascript/flavours/glitch/components/admin/Dimension.jsx @@ -1,11 +1,13 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; -import { FormattedNumber } from 'react-intl'; -import { roundTo10 } from 'flavours/glitch/utils/numbers'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { PureComponent } from 'react'; -export default class Dimension extends React.PureComponent { +import { FormattedNumber } from 'react-intl'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; +import { roundTo10 } from 'flavours/glitch/utils/numbers'; + +export default class Dimension extends PureComponent { static propTypes = { dimension: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx b/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx new file mode 100644 index 0000000000..9ec1460fcf --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedNumber, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; + +export default class ImpactReport extends PureComponent { + + static propTypes = { + domain: PropTypes.string.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { domain } = this.props; + + const params = { + domain: domain, + include_subdomains: true, + }; + + api().post('/api/v1/admin/measures', { + keys: ['instance_accounts', 'instance_follows', 'instance_followers'], + start_at: null, + end_at: null, + instance_accounts: params, + instance_follows: params, + instance_followers: params, + }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + + return ( +
+

+ + + + + + + + + + 0 })}> + + + + + + 0 })}> + + + + + +
+ + + {loading ? : } +
+ + + {loading ? : } +
+ + + {loading ? : } +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx similarity index 92% rename from app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js rename to app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx index 771dbb452d..d72465e4ad 100644 --- a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.js +++ b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx @@ -1,16 +1,19 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; +import { PureComponent } from 'react'; + import { injectIntl, defineMessages } from 'react-intl'; + import classNames from 'classnames'; +import api from 'flavours/glitch/api'; + const messages = defineMessages({ other: { id: 'report.categories.other', defaultMessage: 'Other' }, spam: { id: 'report.categories.spam', defaultMessage: 'Spam' }, violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' }, }); -class Category extends React.PureComponent { +class Category extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -33,7 +36,7 @@ class Category extends React.PureComponent { const { id, text, disabled, selected, children } = this.props; return ( -
+
{selected && }
@@ -52,7 +55,7 @@ class Category extends React.PureComponent { } -class Rule extends React.PureComponent { +class Rule extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -74,7 +77,7 @@ class Rule extends React.PureComponent { const { id, text, disabled, selected } = this.props; return ( -
+
{selected && } {text} @@ -84,8 +87,7 @@ class Rule extends React.PureComponent { } -export default @injectIntl -class ReportReasonSelector extends React.PureComponent { +class ReportReasonSelector extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -157,3 +159,5 @@ class ReportReasonSelector extends React.PureComponent { } } + +export default injectIntl(ReportReasonSelector); diff --git a/app/javascript/flavours/glitch/components/admin/Retention.js b/app/javascript/flavours/glitch/components/admin/Retention.jsx similarity index 97% rename from app/javascript/flavours/glitch/components/admin/Retention.js rename to app/javascript/flavours/glitch/components/admin/Retention.jsx index 9cc39040b9..2cfc30b6fb 100644 --- a/app/javascript/flavours/glitch/components/admin/Retention.js +++ b/app/javascript/flavours/glitch/components/admin/Retention.jsx @@ -1,8 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; +import { PureComponent } from 'react'; + import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; + import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; import { roundTo10 } from 'flavours/glitch/utils/numbers'; const dateForCohort = cohort => { @@ -14,7 +17,7 @@ const dateForCohort = cohort => { } }; -export default class Retention extends React.PureComponent { +export default class Retention extends PureComponent { static propTypes = { start_at: PropTypes.string, @@ -137,7 +140,7 @@ export default class Retention extends React.PureComponent { break; default: title = ; - }; + } return (
diff --git a/app/javascript/flavours/glitch/components/admin/Trends.js b/app/javascript/flavours/glitch/components/admin/Trends.jsx similarity index 95% rename from app/javascript/flavours/glitch/components/admin/Trends.js rename to app/javascript/flavours/glitch/components/admin/Trends.jsx index 774bf36e6e..975ea6e0f2 100644 --- a/app/javascript/flavours/glitch/components/admin/Trends.js +++ b/app/javascript/flavours/glitch/components/admin/Trends.jsx @@ -1,11 +1,14 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import api from 'flavours/glitch/api'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; + import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; import Hashtag from 'flavours/glitch/components/hashtag'; -export default class Trends extends React.PureComponent { +export default class Trends extends PureComponent { static propTypes = { limit: PropTypes.number.isRequired, diff --git a/app/javascript/flavours/glitch/components/animated_number.js b/app/javascript/flavours/glitch/components/animated_number.js deleted file mode 100644 index 9431c96f7b..0000000000 --- a/app/javascript/flavours/glitch/components/animated_number.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ShortNumber from 'mastodon/components/short_number'; -import TransitionMotion from 'react-motion/lib/TransitionMotion'; -import spring from 'react-motion/lib/spring'; -import { reduceMotion } from 'flavours/glitch/initial_state'; - -const obfuscatedCount = count => { - if (count < 0) { - return 0; - } else if (count <= 1) { - return count; - } else { - return '1+'; - } -}; - -export default class AnimatedNumber extends React.PureComponent { - - static propTypes = { - value: PropTypes.number.isRequired, - obfuscate: PropTypes.bool, - }; - - state = { - direction: 1, - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.value > this.props.value) { - this.setState({ direction: 1 }); - } else if (nextProps.value < this.props.value) { - this.setState({ direction: -1 }); - } - } - - willEnter = () => { - const { direction } = this.state; - - return { y: -1 * direction }; - } - - willLeave = () => { - const { direction } = this.state; - - return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) }; - } - - render () { - const { value, obfuscate } = this.props; - const { direction } = this.state; - - if (reduceMotion) { - return obfuscate ? obfuscatedCount(value) : ; - } - - const styles = [{ - key: `${value}`, - data: value, - style: { y: spring(0, { damping: 35, stiffness: 400 }) }, - }]; - - return ( - - {items => ( - - {items.map(({ key, data, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : } - ))} - - )} - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/animated_number.tsx b/app/javascript/flavours/glitch/components/animated_number.tsx new file mode 100644 index 0000000000..81e0af395b --- /dev/null +++ b/app/javascript/flavours/glitch/components/animated_number.tsx @@ -0,0 +1,82 @@ +import { useCallback, useState } from 'react'; +import * as React from 'react'; + +import { TransitionMotion, spring } from 'react-motion'; + +import { reduceMotion } from '../initial_state'; + +import ShortNumber from './short_number'; + +const obfuscatedCount = (count: number) => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + +interface Props { + value: number; + obfuscate?: boolean; +} +export const AnimatedNumber: React.FC = ({ value, obfuscate }) => { + const [previousValue, setPreviousValue] = useState(value); + const [direction, setDirection] = useState<1 | -1>(1); + + if (previousValue !== value) { + setPreviousValue(value); + setDirection(value > previousValue ? 1 : -1); + } + + const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]); + const willLeave = useCallback( + () => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }), + [direction] + ); + + if (reduceMotion) { + return obfuscate ? ( + <>{obfuscatedCount(value)} + ) : ( + + ); + } + + const styles = [ + { + key: `${value}`, + data: value, + style: { y: spring(0, { damping: 35, stiffness: 400 }) }, + }, + ]; + + return ( + + {(items) => ( + + {items.map(({ key, data, style }) => ( + 0 ? 'absolute' : 'static', + transform: `translateY(${style.y * 100}%)`, + }} + > + {obfuscate ? ( + obfuscatedCount(data as number) + ) : ( + + )} + + ))} + + )} + + ); +}; diff --git a/app/javascript/mastodon/components/attachment_list.js b/app/javascript/flavours/glitch/components/attachment_list.jsx similarity index 95% rename from app/javascript/mastodon/components/attachment_list.js rename to app/javascript/flavours/glitch/components/attachment_list.jsx index 0e23889ded..173157b0d5 100644 --- a/app/javascript/mastodon/components/attachment_list.js +++ b/app/javascript/flavours/glitch/components/attachment_list.jsx @@ -1,10 +1,13 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { FormattedMessage } from 'react-intl'; + import classNames from 'classnames'; -import Icon from 'mastodon/components/icon'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Icon } from 'flavours/glitch/components/icon'; const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.js b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx similarity index 89% rename from app/javascript/flavours/glitch/components/autosuggest_emoji.js rename to app/javascript/flavours/glitch/components/autosuggest_emoji.jsx index 83fafbd10d..32a996fd7c 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_emoji.js +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx @@ -1,10 +1,10 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; +import { PureComponent } from 'react'; +import unicodeMapping from 'flavours/glitch/features/emoji/emoji_unicode_mapping_light'; import { assetHost } from 'flavours/glitch/utils/config'; -export default class AutosuggestEmoji extends React.PureComponent { +export default class AutosuggestEmoji extends PureComponent { static propTypes = { emoji: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js b/app/javascript/flavours/glitch/components/autosuggest_hashtag.js deleted file mode 100644 index d787ed07ad..0000000000 --- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ShortNumber from 'flavours/glitch/components/short_number'; -import { FormattedMessage } from 'react-intl'; - -export default class AutosuggestHashtag extends React.PureComponent { - - static propTypes = { - tag: PropTypes.shape({ - name: PropTypes.string.isRequired, - url: PropTypes.string, - history: PropTypes.array, - }).isRequired, - }; - - render() { - const { tag } = this.props; - const weeklyUses = tag.history && ( - total + day.uses * 1, 0)} - /> - ); - - return ( -
-
- #{tag.name} -
- {tag.history !== undefined && ( -
- -
- )} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx new file mode 100644 index 0000000000..932370884a --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx @@ -0,0 +1,42 @@ +import { FormattedMessage } from 'react-intl'; + +import ShortNumber from 'flavours/glitch/components/short_number'; + +interface Props { + tag: { + name: string; + url?: string; + history?: Array<{ + uses: number; + accounts: string; + day: string; + }>; + following?: boolean; + type: 'hashtag'; + }; +} + +export const AutosuggestHashtag: React.FC = ({ tag }) => { + const weeklyUses = tag.history && ( + total + day.uses * 1, 0)} + /> + ); + + return ( +
+
+ #{tag.name} +
+ {tag.history !== undefined && ( +
+ +
+ )} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.js b/app/javascript/flavours/glitch/components/autosuggest_input.jsx similarity index 94% rename from app/javascript/flavours/glitch/components/autosuggest_input.js rename to app/javascript/flavours/glitch/components/autosuggest_input.jsx index b40a2ff350..f0833c8c6b 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.js +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -1,12 +1,15 @@ -import React from 'react'; -import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; -import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import classNames from 'classnames'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; + +import AutosuggestEmoji from './autosuggest_emoji'; +import { AutosuggestHashtag } from './autosuggest_hashtag'; + const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { let word; @@ -50,6 +53,8 @@ export default class AutosuggestInput extends ImmutablePureComponent { id: PropTypes.string, searchTokens: PropTypes.arrayOf(PropTypes.string), maxLength: PropTypes.number, + lang: PropTypes.string, + spellCheck: PropTypes.bool, }; static defaultProps = { @@ -77,7 +82,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { } this.props.onChange(e); - } + }; onKeyDown = (e) => { const { suggestions, disabled } = this.props; @@ -135,24 +140,24 @@ export default class AutosuggestInput extends ImmutablePureComponent { } this.props.onKeyDown(e); - } + }; onBlur = () => { this.setState({ suggestionsHidden: true, focused: false }); - } + }; onFocus = () => { this.setState({ focused: true }); - } + }; onSuggestionClick = (e) => { const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); e.preventDefault(); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.input.focus(); - } + }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } @@ -160,7 +165,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { setInput = (c) => { this.input = c; - } + }; renderSuggestion = (suggestion, i) => { const { selectedSuggestion } = this.state; @@ -178,14 +183,14 @@ export default class AutosuggestInput extends ImmutablePureComponent { } return ( -
+
{inner}
); - } + }; render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength } = this.props; + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, lang, spellCheck } = this.props; const { suggestionsHidden } = this.state; return ( @@ -210,6 +215,8 @@ export default class AutosuggestInput extends ImmutablePureComponent { id={id} className={className} maxLength={maxLength} + lang={lang} + spellCheck={spellCheck} /> diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx similarity index 95% rename from app/javascript/flavours/glitch/components/autosuggest_textarea.js rename to app/javascript/flavours/glitch/components/autosuggest_textarea.jsx index 967c593af2..25ca3fefa5 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.js +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx @@ -1,13 +1,17 @@ -import React from 'react'; -import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; -import AutosuggestEmoji from './autosuggest_emoji'; -import AutosuggestHashtag from './autosuggest_hashtag'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Textarea from 'react-textarea-autosize'; + import classNames from 'classnames'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import Textarea from 'react-textarea-autosize'; + +import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; + +import AutosuggestEmoji from './autosuggest_emoji'; +import { AutosuggestHashtag } from './autosuggest_hashtag'; + const textAtCursorMatchesToken = (str, caretPosition) => { let word; @@ -48,6 +52,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { onKeyDown: PropTypes.func, onPaste: PropTypes.func.isRequired, autoFocus: PropTypes.bool, + lang: PropTypes.string, }; static defaultProps = { @@ -74,7 +79,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } this.props.onChange(e); - } + }; onKeyDown = (e) => { const { suggestions, disabled } = this.props; @@ -132,27 +137,27 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } this.props.onKeyDown(e); - } + }; onBlur = () => { this.setState({ suggestionsHidden: true, focused: false }); - } + }; onFocus = (e) => { this.setState({ focused: true }); if (this.props.onFocus) { this.props.onFocus(e); } - } + }; onSuggestionClick = (e) => { const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); e.preventDefault(); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); this.textarea.focus(); - } + }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } @@ -160,14 +165,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { setTextarea = (c) => { this.textarea = c; - } + }; onPaste = (e) => { if (e.clipboardData && e.clipboardData.files.length === 1) { this.props.onPaste(e.clipboardData.files); e.preventDefault(); } - } + }; renderSuggestion = (suggestion, i) => { const { selectedSuggestion } = this.state; @@ -185,14 +190,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } return ( -
+
{inner}
); - } + }; render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children } = this.props; + const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props; const { suggestionsHidden } = this.state; return [ @@ -216,6 +221,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { onPaste={this.onPaste} dir='auto' aria-autocomplete='list' + lang={lang} />
diff --git a/app/javascript/flavours/glitch/components/avatar.js b/app/javascript/flavours/glitch/components/avatar.js deleted file mode 100644 index 38fd99af59..0000000000 --- a/app/javascript/flavours/glitch/components/avatar.js +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; -import classNames from 'classnames'; - -export default class Avatar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - className: PropTypes.string, - size: PropTypes.number.isRequired, - style: PropTypes.object, - inline: PropTypes.bool, - animate: PropTypes.bool, - }; - - static defaultProps = { - animate: autoPlayGif, - size: 20, - inline: false, - }; - - state = { - hovering: false, - }; - - handleMouseEnter = () => { - if (this.props.animate) return; - this.setState({ hovering: true }); - } - - handleMouseLeave = () => { - if (this.props.animate) return; - this.setState({ hovering: false }); - } - - render () { - const { - account, - animate, - className, - inline, - size, - } = this.props; - const { hovering } = this.state; - - const style = { - ...this.props.style, - width: `${size}px`, - height: `${size}px`, - backgroundSize: `${size}px ${size}px`, - }; - - if (account) { - const src = account.get('avatar'); - const staticSrc = account.get('avatar_static'); - - if (hovering || animate) { - style.backgroundImage = `url(${src})`; - } else { - style.backgroundImage = `url(${staticSrc})`; - } - } - - return ( -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/avatar.tsx b/app/javascript/flavours/glitch/components/avatar.tsx new file mode 100644 index 0000000000..11253a8e94 --- /dev/null +++ b/app/javascript/flavours/glitch/components/avatar.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; + +import classNames from 'classnames'; + +import { useHovering } from 'flavours/glitch/hooks/useHovering'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; +import type { Account } from 'flavours/glitch/types/resources'; + +interface Props { + account: Account | undefined; + className?: string; + size: number; + style?: React.CSSProperties; + inline?: boolean; +} + +export const Avatar: React.FC = ({ + account, + className, + size = 20, + inline = false, + style: styleFromParent, +}) => { + const { hovering, handleMouseEnter, handleMouseLeave } = + useHovering(autoPlayGif); + + const style = { + ...styleFromParent, + width: `${size}px`, + height: `${size}px`, + backgroundSize: `${size}px ${size}px`, + }; + + if (account) { + style.backgroundImage = `url(${account.get( + hovering ? 'avatar' : 'avatar_static' + )})`; + } + + return ( +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/avatar_composite.js b/app/javascript/flavours/glitch/components/avatar_composite.jsx similarity index 83% rename from app/javascript/flavours/glitch/components/avatar_composite.js rename to app/javascript/flavours/glitch/components/avatar_composite.jsx index c0ce7761dc..5503abf4a9 100644 --- a/app/javascript/flavours/glitch/components/avatar_composite.js +++ b/app/javascript/flavours/glitch/components/avatar_composite.jsx @@ -1,9 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; + import { autoPlayGif } from 'flavours/glitch/initial_state'; -export default class AvatarComposite extends React.PureComponent { +export default class AvatarComposite extends PureComponent { static propTypes = { accounts: ImmutablePropTypes.list.isRequired, @@ -79,15 +81,7 @@ export default class AvatarComposite extends React.PureComponent { }; return ( - this.props.onAccountClick(account.get('acct'), e)} - title={`@${account.get('acct')}`} - key={account.get('id')} - > -
- +
); } diff --git a/app/javascript/flavours/glitch/components/avatar_overlay.js b/app/javascript/flavours/glitch/components/avatar_overlay.jsx similarity index 91% rename from app/javascript/flavours/glitch/components/avatar_overlay.js rename to app/javascript/flavours/glitch/components/avatar_overlay.jsx index 01dec587a5..d8215a4785 100644 --- a/app/javascript/flavours/glitch/components/avatar_overlay.js +++ b/app/javascript/flavours/glitch/components/avatar_overlay.jsx @@ -1,9 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; + import { autoPlayGif } from 'flavours/glitch/initial_state'; -export default class AvatarOverlay extends React.PureComponent { +export default class AvatarOverlay extends PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/components/blurhash.js b/app/javascript/flavours/glitch/components/blurhash.js deleted file mode 100644 index 2af5cfc568..0000000000 --- a/app/javascript/flavours/glitch/components/blurhash.js +++ /dev/null @@ -1,65 +0,0 @@ -// @ts-check - -import { decode } from 'blurhash'; -import React, { useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; - -/** - * @typedef BlurhashPropsBase - * @property {string?} hash Hash to render - * @property {number} width - * Width of the blurred region in pixels. Defaults to 32 - * @property {number} [height] - * Height of the blurred region in pixels. Defaults to width - * @property {boolean} [dummy] - * Whether dummy mode is enabled. If enabled, nothing is rendered - * and canvas left untouched - */ - -/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */ - -/** - * Component that is used to render blurred of blurhash string - * - * @param {BlurhashProps} param1 Props of the component - * @returns Canvas which will render blurred region element to embed - */ -function Blurhash({ - hash, - width = 32, - height = width, - dummy = false, - ...canvasProps -}) { - const canvasRef = /** @type {import('react').MutableRefObject} */ (useRef()); - - useEffect(() => { - const { current: canvas } = canvasRef; - canvas.width = canvas.width; // resets canvas - - if (dummy || !hash) return; - - try { - const pixels = decode(hash, width, height); - const ctx = canvas.getContext('2d'); - const imageData = new ImageData(pixels, width, height); - - ctx.putImageData(imageData, 0, 0); - } catch (err) { - console.error('Blurhash decoding failure', { err, hash }); - } - }, [dummy, hash, width, height]); - - return ( - - ); -} - -Blurhash.propTypes = { - hash: PropTypes.string.isRequired, - width: PropTypes.number, - height: PropTypes.number, - dummy: PropTypes.bool, -}; - -export default React.memo(Blurhash); diff --git a/app/javascript/flavours/glitch/components/blurhash.tsx b/app/javascript/flavours/glitch/components/blurhash.tsx new file mode 100644 index 0000000000..d98e7d35db --- /dev/null +++ b/app/javascript/flavours/glitch/components/blurhash.tsx @@ -0,0 +1,49 @@ +import { useRef, useEffect } from 'react'; +import * as React from 'react'; + +import { decode } from 'blurhash'; + +interface Props extends React.HTMLAttributes { + hash: string; + width?: number; + height?: number; + dummy?: boolean; // Whether dummy mode is enabled. If enabled, nothing is rendered and canvas left untouched + children?: never; +} +const Blurhash: React.FC = ({ + hash, + width = 32, + height = width, + dummy = false, + ...canvasProps +}) => { + const canvasRef = useRef(null); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const canvas = canvasRef.current!; + + // eslint-disable-next-line no-self-assign + canvas.width = canvas.width; // resets canvas + + if (dummy || !hash) return; + + try { + const pixels = decode(hash, width, height); + const ctx = canvas.getContext('2d'); + const imageData = new ImageData(pixels, width, height); + + ctx?.putImageData(imageData, 0, 0); + } catch (err) { + console.error('Blurhash decoding failure', { err, hash }); + } + }, [dummy, hash, width, height]); + + return ( + + ); +}; + +const MemoizedBlurhash = React.memo(Blurhash); + +export { MemoizedBlurhash as Blurhash }; diff --git a/app/javascript/flavours/glitch/components/button.js b/app/javascript/flavours/glitch/components/button.jsx similarity index 90% rename from app/javascript/flavours/glitch/components/button.js rename to app/javascript/flavours/glitch/components/button.jsx index b1815c3e19..bdeeeac999 100644 --- a/app/javascript/flavours/glitch/components/button.js +++ b/app/javascript/flavours/glitch/components/button.jsx @@ -1,8 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import classNames from 'classnames'; -export default class Button extends React.PureComponent { +export default class Button extends PureComponent { static propTypes = { text: PropTypes.node, @@ -19,11 +20,11 @@ export default class Button extends React.PureComponent { if (!this.props.disabled) { this.props.onClick(e); } - } + }; setRef = (c) => { this.node = c; - } + }; focus() { this.node.focus(); diff --git a/app/javascript/flavours/glitch/components/check.js b/app/javascript/flavours/glitch/components/check.jsx similarity index 89% rename from app/javascript/flavours/glitch/components/check.js rename to app/javascript/flavours/glitch/components/check.jsx index ee2ef1595a..d818480b7b 100644 --- a/app/javascript/flavours/glitch/components/check.js +++ b/app/javascript/flavours/glitch/components/check.jsx @@ -1,5 +1,3 @@ -import React from 'react'; - const Check = () => ( diff --git a/app/javascript/flavours/glitch/components/circular_progress.tsx b/app/javascript/flavours/glitch/components/circular_progress.tsx new file mode 100644 index 0000000000..850eb93e48 --- /dev/null +++ b/app/javascript/flavours/glitch/components/circular_progress.tsx @@ -0,0 +1,27 @@ +interface Props { + size: number; + strokeWidth: number; +} + +export const CircularProgress: React.FC = ({ size, strokeWidth }) => { + const viewBox = `0 0 ${size} ${size}`; + const radius = (size - strokeWidth) / 2; + + return ( + + + + ); +}; diff --git a/app/javascript/flavours/glitch/components/column.js b/app/javascript/flavours/glitch/components/column.jsx similarity index 70% rename from app/javascript/flavours/glitch/components/column.js rename to app/javascript/flavours/glitch/components/column.jsx index cf0e6d5e40..312a6848b5 100644 --- a/app/javascript/flavours/glitch/components/column.js +++ b/app/javascript/flavours/glitch/components/column.jsx @@ -1,9 +1,13 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { supportsPassiveEvents } from 'detect-passive-events'; + import { scrollTop } from '../scroll'; -export default class Column extends React.PureComponent { +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +export default class Column extends PureComponent { static propTypes = { children: PropTypes.node, @@ -29,25 +33,25 @@ export default class Column extends React.PureComponent { } this._interruptScrollAnimation(); - } + }; setRef = c => { this.node = c; - } + }; componentDidMount () { if (this.props.bindToDocument) { - document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { - this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); + this.node.addEventListener('wheel', this.handleWheel, listenerOptions); } } componentWillUnmount () { if (this.props.bindToDocument) { - document.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('wheel', this.handleWheel, listenerOptions); } else { - this.node.removeEventListener('wheel', this.handleWheel); + this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } } diff --git a/app/javascript/mastodon/components/column_back_button.js b/app/javascript/flavours/glitch/components/column_back_button.jsx similarity index 69% rename from app/javascript/mastodon/components/column_back_button.js rename to app/javascript/flavours/glitch/components/column_back_button.jsx index d97622705e..0934d4b335 100644 --- a/app/javascript/mastodon/components/column_back_button.js +++ b/app/javascript/flavours/glitch/components/column_back_button.jsx @@ -1,10 +1,13 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import Icon from 'mastodon/components/icon'; +import { PureComponent } from 'react'; import { createPortal } from 'react-dom'; -export default class ColumnBackButton extends React.PureComponent { +import { FormattedMessage } from 'react-intl'; + +import { Icon } from 'flavours/glitch/components/icon'; + + +export default class ColumnBackButton extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -15,12 +18,16 @@ export default class ColumnBackButton extends React.PureComponent { }; handleClick = () => { - if (window.history && window.history.length === 1) { - this.context.router.history.push('/'); + const { router } = this.context; + + // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201 + // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location + if (router.route.location.key) { + router.history.goBack(); } else { - this.context.router.history.goBack(); + router.history.push('/'); } - } + }; render () { const { multiColumn } = this.props; diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.js b/app/javascript/flavours/glitch/components/column_back_button_slim.js deleted file mode 100644 index faa0c23a82..0000000000 --- a/app/javascript/flavours/glitch/components/column_back_button_slim.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; -import Icon from 'flavours/glitch/components/icon'; - -export default class ColumnBackButtonSlim extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - handleClick = (event) => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history.state) { - const state = this.context.router.history.location.state; - if (event.shiftKey && state && state.mastodonBackSteps) { - this.context.router.history.go(-state.mastodonBackSteps); - } else { - this.context.router.history.goBack(); - } - } else { - this.context.router.history.push('/'); - } - } - - render () { - return ( -
-
- - -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx new file mode 100644 index 0000000000..7b3bac45f9 --- /dev/null +++ b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Icon } from 'flavours/glitch/components/icon'; + +export default class ColumnBackButtonSlim extends PureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + handleClick = () => { + const { router } = this.context; + + // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201 + // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location + if (router.route.location.key) { + router.history.goBack(); + } else { + router.history.push('/'); + } + }; + + render () { + return ( +
+
+ + +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/column_header.js b/app/javascript/flavours/glitch/components/column_header.jsx similarity index 90% rename from app/javascript/flavours/glitch/components/column_header.js rename to app/javascript/flavours/glitch/components/column_header.jsx index 0f89b3a97b..e8c056c0bf 100644 --- a/app/javascript/flavours/glitch/components/column_header.js +++ b/app/javascript/flavours/glitch/components/column_header.jsx @@ -1,9 +1,12 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; import { createPortal } from 'react-dom'; -import classNames from 'classnames'; + import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; + +import classNames from 'classnames'; + +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, @@ -12,8 +15,7 @@ const messages = defineMessages({ moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, }); -export default @injectIntl -class ColumnHeader extends React.PureComponent { +class ColumnHeader extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -43,51 +45,46 @@ class ColumnHeader extends React.PureComponent { animating: false, }; - historyBack = (skip) => { - // if history is exhausted, or we would leave mastodon, just go to root. - if (window.history.state) { - const state = this.context.router.history.location.state; - if (skip && state && state.mastodonBackSteps) { - this.context.router.history.go(-state.mastodonBackSteps); - } else { - this.context.router.history.goBack(); - } - } else { - this.context.router.history.push('/'); - } - } - handleToggleClick = (e) => { e.stopPropagation(); this.setState({ collapsed: !this.state.collapsed, animating: true }); - } + }; handleTitleClick = () => { this.props.onClick?.(); - } + }; handleMoveLeft = () => { this.props.onMove(-1); - } + }; handleMoveRight = () => { this.props.onMove(1); - } + }; - handleBackClick = (event) => { - this.historyBack(event.shiftKey); - } + handleBackClick = () => { + const { router } = this.context; + + // Check if there is a previous page in the app to go back to per https://stackoverflow.com/a/70532858/9703201 + // When upgrading to V6, check `location.key !== 'default'` instead per https://github.com/remix-run/history/blob/main/docs/api-reference.md#location + if (router.route.location.key) { + router.history.goBack(); + } else { + router.history.push('/'); + } + }; handleTransitionEnd = () => { this.setState({ animating: false }); - } + }; handlePin = () => { if (!this.props.pinned) { - this.historyBack(); + this.context.router.history.replace('/'); } + this.props.onPin(); - } + }; render () { const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props; @@ -218,3 +215,5 @@ class ColumnHeader extends React.PureComponent { } } + +export default injectIntl(ColumnHeader); diff --git a/app/javascript/flavours/glitch/components/common_counter.js b/app/javascript/flavours/glitch/components/common_counter.jsx similarity index 98% rename from app/javascript/flavours/glitch/components/common_counter.js rename to app/javascript/flavours/glitch/components/common_counter.jsx index dd9b62de9b..785907bd25 100644 --- a/app/javascript/flavours/glitch/components/common_counter.js +++ b/app/javascript/flavours/glitch/components/common_counter.jsx @@ -1,10 +1,7 @@ // @ts-check -import React from 'react'; 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 diff --git a/app/javascript/flavours/glitch/components/dismissable_banner.js b/app/javascript/flavours/glitch/components/dismissable_banner.jsx similarity index 85% rename from app/javascript/flavours/glitch/components/dismissable_banner.js rename to app/javascript/flavours/glitch/components/dismissable_banner.jsx index ff52a619d2..21063c9ed6 100644 --- a/app/javascript/flavours/glitch/components/dismissable_banner.js +++ b/app/javascript/flavours/glitch/components/dismissable_banner.jsx @@ -1,15 +1,17 @@ -import React from 'react'; -import IconButton from './icon_button'; 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' }, }); -export default @injectIntl -class DismissableBanner extends React.PureComponent { +class DismissableBanner extends PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -24,7 +26,7 @@ class DismissableBanner extends React.PureComponent { handleDismiss = () => { const { id } = this.props; this.setState({ visible: false }, () => bannerSettings.set(id, true)); - } + }; render () { const { visible } = this.state; @@ -49,3 +51,5 @@ class DismissableBanner extends React.PureComponent { } } + +export default injectIntl(DismissableBanner); diff --git a/app/javascript/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js deleted file mode 100644 index 1c22975785..0000000000 --- a/app/javascript/flavours/glitch/components/display_name.js +++ /dev/null @@ -1,102 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { autoPlayGif } from 'flavours/glitch/initial_state'; -import Skeleton from 'flavours/glitch/components/skeleton'; - -export default class DisplayName extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map, - className: PropTypes.string, - inline: PropTypes.bool, - localDomain: PropTypes.string, - others: ImmutablePropTypes.list, - handleClick: PropTypes.func, - }; - - handleMouseEnter = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - } - - handleMouseLeave = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - } - - render() { - const { account, className, inline, localDomain, others, onAccountClick } = this.props; - - const computedClass = classNames('display-name', { inline }, className); - - let displayName, suffix; - let acct; - - if (account) { - acct = account.get('acct'); - - if (acct.indexOf('@') === -1 && localDomain) { - acct = `${acct}@${localDomain}`; - } - } - - if (others && others.size > 0) { - displayName = others.take(2).map(a => ( - onAccountClick(a.get('acct'), e)} - title={`@${a.get('acct')}`} - rel='noopener noreferrer' - > - - - - - )).reduce((prev, cur) => [prev, ', ', cur]); - - if (others.size - 2 > 0) { - displayName.push(` +${others.size - 2}`); - } - - suffix = ( - onAccountClick(account.get('acct'), e)} rel='noopener noreferrer'> - @{acct} - - ); - } else if (account) { - displayName = ; - suffix = @{acct}; - } else { - displayName = ; - suffix = ; - } - - return ( - - {displayName} - {inline ? ' ' : null} - {suffix} - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/display_name.tsx b/app/javascript/flavours/glitch/components/display_name.tsx new file mode 100644 index 0000000000..7224ac3d73 --- /dev/null +++ b/app/javascript/flavours/glitch/components/display_name.tsx @@ -0,0 +1,124 @@ +import React from 'react'; + +import classNames from 'classnames'; + +import type { List } from 'immutable'; + +import type { Account } from 'flavours/glitch/types/resources'; + +import { autoPlayGif } from '../initial_state'; + +import { Skeleton } from './skeleton'; + +interface Props { + account: Account; + others: List; + localDomain: string; + inline?: boolean; +} +export class DisplayName extends React.PureComponent { + handleMouseEnter: React.ReactEventHandler = ({ + currentTarget, + }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('img.custom-emoji'); + + emojis.forEach((emoji) => { + const originalSrc = emoji.getAttribute('data-original'); + if (originalSrc != null) emoji.src = originalSrc; + }); + }; + + handleMouseLeave: React.ReactEventHandler = ({ + currentTarget, + }) => { + if (autoPlayGif) { + return; + } + + const emojis = + currentTarget.querySelectorAll('img.custom-emoji'); + + emojis.forEach((emoji) => { + const staticSrc = emoji.getAttribute('data-static'); + if (staticSrc != null) emoji.src = staticSrc; + }); + }; + + render() { + const { others, localDomain, inline } = this.props; + + let displayName: React.ReactNode, suffix: React.ReactNode, account: Account; + + if (others && others.size > 1) { + displayName = others + .take(2) + .map((a) => ( + + + + )) + .reduce((prev, cur) => [prev, ', ', cur]); + + if (others.size - 2 > 0) { + suffix = `+${others.size - 2}`; + } + } else if ((others && others.size > 0) || this.props.account) { + if (others && others.size > 0) { + account = others.first(); + } else { + account = this.props.account; + } + + let acct = account.get('acct'); + + if (acct.indexOf('@') === -1 && localDomain) { + acct = `${acct}@${localDomain}`; + } + + displayName = ( + + + + ); + suffix = @{acct}; + } else { + displayName = ( + + + + + + ); + suffix = ( + + + + ); + } + + return ( + + {displayName} + {inline ? ' ' : null} + {suffix} + + ); + } +} diff --git a/app/javascript/flavours/glitch/components/domain.js b/app/javascript/flavours/glitch/components/domain.js deleted file mode 100644 index 697065d874..0000000000 --- a/app/javascript/flavours/glitch/components/domain.js +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import IconButton from './icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -const messages = defineMessages({ - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, -}); - -export default @injectIntl -class Account extends ImmutablePureComponent { - - static propTypes = { - domain: PropTypes.string, - onUnblockDomain: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleDomainUnblock = () => { - this.props.onUnblockDomain(this.props.domain); - } - - render () { - const { domain, intl } = this.props; - - return ( -
-
- - {domain} - - -
- -
-
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/components/domain.tsx b/app/javascript/flavours/glitch/components/domain.tsx new file mode 100644 index 0000000000..50c5c256ec --- /dev/null +++ b/app/javascript/flavours/glitch/components/domain.tsx @@ -0,0 +1,45 @@ +import { useCallback } from 'react'; +import * as React from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { IconButton } from './icon_button'; + +const messages = defineMessages({ + unblockDomain: { + id: 'account.unblock_domain', + defaultMessage: 'Unblock domain {domain}', + }, +}); + +interface Props { + domain: string; + onUnblockDomain: (domain: string) => void; +} + +export const Domain: React.FC = ({ domain, onUnblockDomain }) => { + const intl = useIntl(); + + const handleDomainUnblock = useCallback(() => { + onUnblockDomain(domain); + }, [domain, onUnblockDomain]); + + return ( +
+
+ + {domain} + + +
+ +
+
+
+ ); +}; diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.jsx similarity index 89% rename from app/javascript/mastodon/components/dropdown_menu.js rename to app/javascript/flavours/glitch/components/dropdown_menu.jsx index 5897aada89..0416df5d45 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx @@ -1,16 +1,20 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import IconButton from './icon_button'; -import Overlay from 'react-overlays/Overlay'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import classNames from 'classnames'; -import { CircularProgress } from 'mastodon/components/loading_indicator'; +import { PureComponent, cloneElement, Children } from 'react'; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import { supportsPassiveEvents } from 'detect-passive-events'; +import Overlay from 'react-overlays/Overlay'; + +import { CircularProgress } from "./circular_progress"; +import { IconButton } from './icon_button'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; let id = 0; -class DropdownMenu extends React.PureComponent { +class DropdownMenu extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -35,12 +39,13 @@ class DropdownMenu extends React.PureComponent { handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } - } + }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('keydown', this.handleKeyDown, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('keydown', this.handleKeyDown, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem && this.props.openedViaKeyboard) { @@ -49,18 +54,18 @@ class DropdownMenu extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('keydown', this.handleKeyDown, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } setRef = c => { this.node = c; - } + }; setFocusRef = c => { this.focusedItem = c; - } + }; handleKeyDown = e => { const items = Array.from(this.node.querySelectorAll('a, button')); @@ -97,34 +102,34 @@ class DropdownMenu extends React.PureComponent { e.preventDefault(); e.stopPropagation(); } - } + }; handleItemKeyPress = e => { if (e.key === 'Enter' || e.key === ' ') { this.handleClick(e); } - } + }; handleClick = e => { const { onItemClick } = this.props; onItemClick(e); - } + }; renderItem = (option, i) => { if (option === null) { return
  • ; } - const { text, href = '#', target = '_blank', method } = option; + const { text, href = '#', target = '_blank', method, dangerous } = option; return ( -
  • - +
  • + {text}
  • ); - } + }; render () { const { items, scrollable, renderHeader, loading } = this.props; @@ -154,7 +159,7 @@ class DropdownMenu extends React.PureComponent { } -export default class Dropdown extends React.PureComponent { +export default class Dropdown extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -194,7 +199,7 @@ export default class Dropdown extends React.PureComponent { } else { this.props.onOpen(this.state.id, this.handleItemClick, type !== 'click'); } - } + }; handleClose = () => { if (this.activeElement) { @@ -202,13 +207,13 @@ export default class Dropdown extends React.PureComponent { this.activeElement = null; } this.props.onClose(this.state.id); - } + }; handleMouseDown = () => { if (!this.state.open) { this.activeElement = document.activeElement; } - } + }; handleButtonKeyDown = (e) => { switch(e.key) { @@ -217,7 +222,7 @@ export default class Dropdown extends React.PureComponent { this.handleMouseDown(); break; } - } + }; handleKeyPress = (e) => { switch(e.key) { @@ -228,7 +233,7 @@ export default class Dropdown extends React.PureComponent { e.preventDefault(); break; } - } + }; handleItemClick = e => { const { onItemClick } = this.props; @@ -247,25 +252,25 @@ export default class Dropdown extends React.PureComponent { e.preventDefault(); this.context.router.history.push(item.to); } - } + }; setTargetRef = c => { this.target = c; - } + }; findTarget = () => { return this.target; - } + }; componentWillUnmount = () => { if (this.state.id === this.props.openDropdownId) { this.handleClose(); } - } + }; close = () => { this.handleClose(); - } + }; render () { const { @@ -285,7 +290,7 @@ export default class Dropdown extends React.PureComponent { const open = this.state.id === openDropdownId; - const button = children ? React.cloneElement(React.Children.only(children), { + const button = children ? cloneElement(Children.only(children), { onClick: this.handleClick, onMouseDown: this.handleMouseDown, onKeyDown: this.handleButtonKeyDown, @@ -305,7 +310,7 @@ export default class Dropdown extends React.PureComponent { ); return ( - + <> {button} @@ -328,7 +333,7 @@ export default class Dropdown extends React.PureComponent {
    )} - + ); } diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js index a1519757de..7c9c167137 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js @@ -1,4 +1,5 @@ import { connect } from 'react-redux'; + import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; import { fetchHistory } from 'flavours/glitch/actions/history'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; diff --git a/app/javascript/flavours/glitch/components/edited_timestamp/index.js b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx similarity index 81% rename from app/javascript/flavours/glitch/components/edited_timestamp/index.js rename to app/javascript/flavours/glitch/components/edited_timestamp/index.jsx index 9648133aff..3dbac58b54 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/index.js +++ b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx @@ -1,24 +1,29 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { FormattedMessage, injectIntl } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; -import DropdownMenu from './containers/dropdown_menu_container'; + import { connect } from 'react-redux'; + import { openModal } from 'flavours/glitch/actions/modal'; -import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import { Icon } from 'flavours/glitch/components/icon'; import InlineAccount from 'flavours/glitch/components/inline_account'; +import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; + +import DropdownMenu from './containers/dropdown_menu_container'; const mapDispatchToProps = (dispatch, { statusId }) => ({ onItemClick (index) { - dispatch(openModal('COMPARE_HISTORY', { index, statusId })); + dispatch(openModal({ + modalType: 'COMPARE_HISTORY', + modalProps: { index, statusId }, + })); }, }); -export default @connect(null, mapDispatchToProps) -@injectIntl -class EditedTimestamp extends React.PureComponent { +class EditedTimestamp extends PureComponent { static propTypes = { statusId: PropTypes.string.isRequired, @@ -34,9 +39,9 @@ class EditedTimestamp extends React.PureComponent { renderHeader = items => { return ( - + ); - } + }; renderItem = (item, index, { onClick, onKeyPress }) => { const formattedDate = ; @@ -53,7 +58,7 @@ class EditedTimestamp extends React.PureComponent { ); - } + }; render () { const { timestamp, intl, statusId } = this.props; @@ -68,3 +73,5 @@ class EditedTimestamp extends React.PureComponent { } } + +export default connect(null, mapDispatchToProps)(injectIntl(EditedTimestamp)); diff --git a/app/javascript/flavours/glitch/components/error_boundary.js b/app/javascript/flavours/glitch/components/error_boundary.jsx similarity index 97% rename from app/javascript/flavours/glitch/components/error_boundary.js rename to app/javascript/flavours/glitch/components/error_boundary.jsx index e0ca3e2b05..4a4dadf0a5 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.js +++ b/app/javascript/flavours/glitch/components/error_boundary.jsx @@ -1,12 +1,16 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; -import { source_url } from 'flavours/glitch/initial_state'; -import { preferencesLink } from 'flavours/glitch/utils/backend_links'; -import StackTrace from 'stacktrace-js'; + import { Helmet } from 'react-helmet'; -export default class ErrorBoundary extends React.PureComponent { +import StackTrace from 'stacktrace-js'; + +import { source_url } from 'flavours/glitch/initial_state'; +import { preferencesLink } from 'flavours/glitch/utils/backend_links'; + +export default class ErrorBoundary extends PureComponent { static propTypes = { children: PropTypes.node, @@ -18,7 +22,7 @@ export default class ErrorBoundary extends React.PureComponent { stackTrace: undefined, mappedStackTrace: undefined, componentStack: undefined, - } + }; componentDidCatch(error, info) { this.setState({ @@ -72,7 +76,7 @@ export default class ErrorBoundary extends React.PureComponent { } return ( -
    +

    diff --git a/app/javascript/flavours/glitch/components/gifv.js b/app/javascript/flavours/glitch/components/gifv.js deleted file mode 100644 index b775e52005..0000000000 --- a/app/javascript/flavours/glitch/components/gifv.js +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class GIFV extends React.PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - onClick: PropTypes.func, - }; - - state = { - loading: true, - }; - - handleLoadedData = () => { - this.setState({ loading: false }); - } - - componentWillReceiveProps (nextProps) { - if (nextProps.src !== this.props.src) { - this.setState({ loading: true }); - } - } - - handleClick = e => { - const { onClick } = this.props; - - if (onClick) { - e.stopPropagation(); - onClick(); - } - } - - render () { - const { src, width, height, alt } = this.props; - const { loading } = this.state; - - return ( -

    - {loading && ( - - )} - -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/gifv.tsx b/app/javascript/flavours/glitch/components/gifv.tsx new file mode 100644 index 0000000000..c9b9de29b6 --- /dev/null +++ b/app/javascript/flavours/glitch/components/gifv.tsx @@ -0,0 +1,71 @@ +import { useCallback, useState } from 'react'; +import * as React from 'react'; + +interface Props { + src: string; + key: string; + alt?: string; + lang?: string; + width: number; + height: number; + onClick?: () => void; +} + +export const GIFV: React.FC = ({ + src, + alt, + lang, + width, + height, + onClick, +}) => { + const [loading, setLoading] = useState(true); + + const handleLoadedData: React.ReactEventHandler = + useCallback(() => { + setLoading(false); + }, [setLoading]); + + const handleClick: React.MouseEventHandler = useCallback( + (e) => { + if (onClick) { + e.stopPropagation(); + onClick(); + } + }, + [onClick] + ); + + return ( +
    + {loading && ( + + )} + +
    + ); +}; diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.jsx similarity index 90% rename from app/javascript/flavours/glitch/components/hashtag.js rename to app/javascript/flavours/glitch/components/hashtag.jsx index 422b9a8fa1..422ead01db 100644 --- a/app/javascript/flavours/glitch/components/hashtag.js +++ b/app/javascript/flavours/glitch/components/hashtag.jsx @@ -1,15 +1,21 @@ // @ts-check -import React from 'react'; -import { Sparklines, SparklinesCurve } from 'react-sparklines'; -import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Permalink from './permalink'; -import ShortNumber from 'flavours/glitch/components/short_number'; -import Skeleton from 'flavours/glitch/components/skeleton'; +import { Component } from 'react'; + +import { FormattedMessage } from 'react-intl'; + import classNames from 'classnames'; -class SilentErrorBoundary extends React.Component { +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import { Sparklines, SparklinesCurve } from 'react-sparklines'; + +import ShortNumber from 'flavours/glitch/components/short_number'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; + +import Permalink from './permalink'; + +class SilentErrorBoundary extends Component { static propTypes = { children: PropTypes.node, @@ -35,13 +41,12 @@ class SilentErrorBoundary extends React.Component { /** * Used to render counter of how much people are talking about hashtag - * * @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element} */ export const accountsCountRenderer = (displayNumber, pluralReady) => ( {displayNumber}
    , @@ -50,12 +55,14 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => ( /> ); +// @ts-expect-error export const ImmutableHashtag = ({ hashtag }) => ( day.get('uses')).toArray()} /> ); @@ -64,11 +71,12 @@ ImmutableHashtag.propTypes = { hashtag: ImmutablePropTypes.map.isRequired, }; +// @ts-expect-error const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
    - {name ? #{name} : } + {name ? <>#{name} : } {description ? ( diff --git a/app/javascript/flavours/glitch/components/icon.js b/app/javascript/flavours/glitch/components/icon.js deleted file mode 100644 index d8a17722fe..0000000000 --- a/app/javascript/flavours/glitch/components/icon.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class Icon extends React.PureComponent { - - static propTypes = { - id: PropTypes.string.isRequired, - className: PropTypes.string, - fixedWidth: PropTypes.bool, - }; - - render () { - const { id, className, fixedWidth, ...other } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/icon.tsx b/app/javascript/flavours/glitch/components/icon.tsx new file mode 100644 index 0000000000..765aa89aec --- /dev/null +++ b/app/javascript/flavours/glitch/components/icon.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; + +import classNames from 'classnames'; + +interface Props extends React.HTMLAttributes { + id: string; + className?: string; + fixedWidth?: boolean; + children?: never; +} + +export const Icon: React.FC = ({ + id, + className, + fixedWidth, + ...other +}) => ( + +); diff --git a/app/javascript/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.tsx similarity index 59% rename from app/javascript/flavours/glitch/components/icon_button.js rename to app/javascript/flavours/glitch/components/icon_button.tsx index 2485f0f480..ecc418773c 100644 --- a/app/javascript/flavours/glitch/components/icon_button.js +++ b/app/javascript/flavours/glitch/components/icon_button.tsx @@ -1,54 +1,55 @@ -import React from 'react'; -import Motion from '../features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import PropTypes from 'prop-types'; +import * as React from 'react'; + import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import AnimatedNumber from 'flavours/glitch/components/animated_number'; -export default class IconButton extends React.PureComponent { - - static propTypes = { - className: PropTypes.string, - title: PropTypes.string.isRequired, - icon: PropTypes.string.isRequired, - onClick: PropTypes.func, - onMouseDown: PropTypes.func, - onKeyDown: PropTypes.func, - onKeyPress: PropTypes.func, - size: PropTypes.number, - active: PropTypes.bool, - expanded: PropTypes.bool, - style: PropTypes.object, - activeStyle: PropTypes.object, - disabled: PropTypes.bool, - inverted: PropTypes.bool, - animate: PropTypes.bool, - overlay: PropTypes.bool, - tabIndex: PropTypes.string, - label: PropTypes.string, - counter: PropTypes.number, - obfuscateCount: PropTypes.bool, - href: PropTypes.string, - ariaHidden: PropTypes.bool, - }; +import { AnimatedNumber } from './animated_number'; +import { Icon } from './icon'; +interface Props { + className?: string; + title: string; + icon: string; + onClick?: React.MouseEventHandler; + onMouseDown?: React.MouseEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onKeyPress?: React.KeyboardEventHandler; + size: number; + active: boolean; + expanded?: boolean; + style?: React.CSSProperties; + activeStyle?: React.CSSProperties; + disabled: boolean; + inverted?: boolean; + animate: boolean; + overlay: boolean; + tabIndex: number; + label?: string; + counter?: number; + obfuscateCount?: boolean; + href?: string; + ariaHidden: boolean; +} +interface States { + activate: boolean; + deactivate: boolean; +} +export class IconButton extends React.PureComponent { static defaultProps = { size: 18, active: false, disabled: false, animate: false, overlay: false, - tabIndex: '0', + tabIndex: 0, ariaHidden: false, }; state = { activate: false, deactivate: false, - } + }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { if (!nextProps.animate) return; if (this.props.active && !nextProps.active) { @@ -58,40 +59,40 @@ export default class IconButton extends React.PureComponent { } } - handleClick = (e) => { + handleClick: React.MouseEventHandler = (e) => { e.preventDefault(); - if (!this.props.disabled) { + if (!this.props.disabled && this.props.onClick != null) { this.props.onClick(e); } - } + }; - handleKeyPress = (e) => { + handleKeyPress: React.KeyboardEventHandler = (e) => { if (this.props.onKeyPress && !this.props.disabled) { this.props.onKeyPress(e); } - } + }; - handleMouseDown = (e) => { + handleMouseDown: React.MouseEventHandler = (e) => { if (!this.props.disabled && this.props.onMouseDown) { this.props.onMouseDown(e); } - } + }; - handleKeyDown = (e) => { + handleKeyDown: React.KeyboardEventHandler = (e) => { if (!this.props.disabled && this.props.onKeyDown) { this.props.onKeyDown(e); } - } + }; - render () { + render() { // Hack required for some icons which have an overriden size let containerSize = '1.28571429em'; if (this.props.style?.fontSize) { containerSize = `${this.props.size * 1.28571429}px`; } - let style = { + const style = { fontSize: `${this.props.size}px`, height: containerSize, lineHeight: `${this.props.size}px`, @@ -120,10 +121,7 @@ export default class IconButton extends React.PureComponent { ariaHidden, } = this.props; - const { - activate, - deactivate, - } = this.state; + const { activate, deactivate } = this.state; const classes = classNames(className, 'icon-button', { active, @@ -140,13 +138,18 @@ export default class IconButton extends React.PureComponent { } let contents = ( - - + ); - if (href && !this.prop) { + if (href != null) { contents = ( {contents} @@ -156,6 +159,7 @@ export default class IconButton extends React.PureComponent { return ( - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/load_gap.tsx b/app/javascript/flavours/glitch/components/load_gap.tsx new file mode 100644 index 0000000000..c86e70f9fc --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_gap.tsx @@ -0,0 +1,34 @@ +import { useCallback } from 'react'; + +import { useIntl, defineMessages } from 'react-intl'; + +import { Icon } from 'flavours/glitch/components/icon'; + +const messages = defineMessages({ + load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, +}); + +interface Props { + disabled: boolean; + maxId: string; + onClick: (maxId: string) => void; +} + +export const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { + const intl = useIntl(); + + const handleClick = useCallback(() => { + onClick(maxId); + }, [maxId, onClick]); + + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/load_more.js b/app/javascript/flavours/glitch/components/load_more.js deleted file mode 100644 index 389c3e1e11..0000000000 --- a/app/javascript/flavours/glitch/components/load_more.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class LoadMore extends React.PureComponent { - - static propTypes = { - onClick: PropTypes.func, - disabled: PropTypes.bool, - visible: PropTypes.bool, - } - - static defaultProps = { - visible: true, - } - - render() { - const { disabled, visible } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/load_more.tsx b/app/javascript/flavours/glitch/components/load_more.tsx new file mode 100644 index 0000000000..8b5746ad30 --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_more.tsx @@ -0,0 +1,24 @@ +import { FormattedMessage } from 'react-intl'; + +interface Props { + onClick: (event: React.MouseEvent) => void; + disabled?: boolean; + visible?: boolean; +} +export const LoadMore: React.FC = ({ + onClick, + disabled, + visible = true, +}) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/load_pending.js b/app/javascript/flavours/glitch/components/load_pending.js deleted file mode 100644 index 7e27024036..0000000000 --- a/app/javascript/flavours/glitch/components/load_pending.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import PropTypes from 'prop-types'; - -export default class LoadPending extends React.PureComponent { - - static propTypes = { - onClick: PropTypes.func, - count: PropTypes.number, - } - - render() { - const { count } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/load_pending.tsx b/app/javascript/flavours/glitch/components/load_pending.tsx new file mode 100644 index 0000000000..f7589622ed --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_pending.tsx @@ -0,0 +1,18 @@ +import { FormattedMessage } from 'react-intl'; + +interface Props { + onClick: (event: React.MouseEvent) => void; + count: number; +} + +export const LoadPending: React.FC = ({ onClick, count }) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/loading_indicator.js b/app/javascript/flavours/glitch/components/loading_indicator.js deleted file mode 100644 index 59f721c50f..0000000000 --- a/app/javascript/flavours/glitch/components/loading_indicator.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export const CircularProgress = ({ size, strokeWidth }) => { - const viewBox = `0 0 ${size} ${size}`; - const radius = (size - strokeWidth) / 2; - - return ( - - - - ); -}; - -CircularProgress.propTypes = { - size: PropTypes.number.isRequired, - strokeWidth: PropTypes.number.isRequired, -}; - -const LoadingIndicator = () => ( -
    - -
    -); - -export default LoadingIndicator; diff --git a/app/javascript/flavours/glitch/components/loading_indicator.tsx b/app/javascript/flavours/glitch/components/loading_indicator.tsx new file mode 100644 index 0000000000..6bc24a0d61 --- /dev/null +++ b/app/javascript/flavours/glitch/components/loading_indicator.tsx @@ -0,0 +1,7 @@ +import { CircularProgress } from './circular_progress'; + +export const LoadingIndicator: React.FC = () => ( +
    + +
    +); diff --git a/app/javascript/flavours/glitch/components/logo.js b/app/javascript/flavours/glitch/components/logo.js deleted file mode 100644 index ee5c22496c..0000000000 --- a/app/javascript/flavours/glitch/components/logo.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -const Logo = () => ( - - Mastodon - - -); - -export default Logo; diff --git a/app/javascript/flavours/glitch/components/logo.jsx b/app/javascript/flavours/glitch/components/logo.jsx new file mode 100644 index 0000000000..16ca9f80fd --- /dev/null +++ b/app/javascript/flavours/glitch/components/logo.jsx @@ -0,0 +1,14 @@ +import logo from 'mastodon/../images/logo.svg'; + +export const WordmarkLogo = () => ( + + Mastodon + + +); + +export const SymbolLogo = () => ( + Mastodon +); + +export default WordmarkLogo; diff --git a/app/javascript/flavours/glitch/components/media_attachments.js b/app/javascript/flavours/glitch/components/media_attachments.jsx similarity index 86% rename from app/javascript/flavours/glitch/components/media_attachments.js rename to app/javascript/flavours/glitch/components/media_attachments.jsx index a517fcf300..4e777437a0 100644 --- a/app/javascript/flavours/glitch/components/media_attachments.js +++ b/app/javascript/flavours/glitch/components/media_attachments.jsx @@ -1,15 +1,18 @@ -import React from 'react'; import PropTypes from 'prop-types'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video, Audio } from 'flavours/glitch/features/ui/util/async-components'; -import Bundle from 'flavours/glitch/features/ui/components/bundle'; + import noop from 'lodash/noop'; +import Bundle from 'flavours/glitch/features/ui/components/bundle'; +import { MediaGallery, Video, Audio } from 'flavours/glitch/features/ui/util/async-components'; + export default class MediaAttachments extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, + lang: PropTypes.string, height: PropTypes.number, width: PropTypes.number, revealed: PropTypes.bool, @@ -30,7 +33,7 @@ export default class MediaAttachments extends ImmutablePureComponent { return (
    ); - } + }; renderLoadingVideoPlayer = () => { const { height, width } = this.props; @@ -38,7 +41,7 @@ export default class MediaAttachments extends ImmutablePureComponent { return (
    ); - } + }; renderLoadingAudioPlayer = () => { const { height, width } = this.props; @@ -46,11 +49,12 @@ export default class MediaAttachments extends ImmutablePureComponent { return (
    ); - } + }; render () { const { status, width, height, revealed } = this.props; const mediaAttachments = status.get('media_attachments'); + const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang; if (mediaAttachments.size === 0) { return null; @@ -58,13 +62,15 @@ export default class MediaAttachments extends ImmutablePureComponent { if (mediaAttachments.getIn([0, 'type']) === 'audio') { const audio = mediaAttachments.get(0); + const description = audio.getIn(['translation', 'description']) || audio.get('description'); return ( {Component => ( @@ -87,7 +94,8 @@ export default class MediaAttachments extends ImmutablePureComponent { frameRate={video.getIn(['meta', 'original', 'frame_rate'])} blurhash={video.get('blurhash')} src={video.get('url')} - alt={video.get('description')} + alt={description} + lang={language} width={width} height={height} inline @@ -104,6 +112,7 @@ export default class MediaAttachments extends ImmutablePureComponent { {Component => ( { if (this.hoverToPlay()) { e.target.pause(); e.target.currentTime = 0; } - } + }; getAutoPlay() { return this.props.autoplay || autoPlayGif; @@ -91,21 +98,19 @@ class Item extends React.PureComponent { } e.stopPropagation(); - } + }; handleImageLoad = () => { this.setState({ loaded: true }); - } + }; render () { - const { attachment, index, size, standalone, letterbox, displayWidth, visible } = this.props; + const { attachment, lang, index, size, standalone, letterbox, displayWidth, visible } = this.props; + + let badges = [], thumbnail; let width = 50; let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; if (size === 1) { width = 100; @@ -115,46 +120,16 @@ class Item extends React.PureComponent { height = 50; } - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } + if (attachment.get('description')?.length > 0) { + badges.push(ALT); } - let thumbnail = ''; + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); if (attachment.get('type') === 'unknown') { return ( -
    - +
    + @@ -203,12 +179,15 @@ class Item extends React.PureComponent { } else if (attachment.get('type') === 'gifv') { const autoPlay = this.getAutoPlay(); + badges.push(GIF); + thumbnail = (
    ); } return ( -
    +
    + {visible && thumbnail} + + {badges && ( +
    + {badges} +
    + )}
    ); } } -export default @injectIntl -class MediaGallery extends React.PureComponent { +class MediaGallery extends PureComponent { static propTypes = { sensitive: PropTypes.bool, @@ -251,6 +234,7 @@ class MediaGallery extends React.PureComponent { fullwidth: PropTypes.bool, hidden: PropTypes.bool, media: ImmutablePropTypes.list.isRequired, + lang: PropTypes.string, size: PropTypes.object, onOpenMedia: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -278,7 +262,7 @@ class MediaGallery extends React.PureComponent { window.removeEventListener('resize', this.handleResize); } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) { this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' }); } else if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { @@ -286,7 +270,7 @@ class MediaGallery extends React.PureComponent { } } - componentDidUpdate (prevProps) { + componentDidUpdate () { if (this.node) { this.handleResize(); } @@ -307,11 +291,11 @@ class MediaGallery extends React.PureComponent { } else { this.setState({ visible: !this.state.visible }); } - } + }; handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index); - } + this.props.onOpenMedia(this.props.media, index, this.props.lang); + }; handleRef = (node) => { this.node = node; @@ -319,12 +303,12 @@ class MediaGallery extends React.PureComponent { if (this.node) { this._setDimensions(); } - } + }; _setDimensions () { const width = this.node.offsetWidth; - - if (width && width != this.state.width) { + + if (width && width !== this.state.width) { // offsetWidth triggers a layout, so only calculate when we need to if (this.props.cacheWidth) { this.props.cacheWidth(width); @@ -342,7 +326,7 @@ class MediaGallery extends React.PureComponent { } render () { - const { media, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props; + const { media, lang, intl, sensitive, letterbox, fullwidth, defaultWidth, autoplay } = this.props; const { visible } = this.state; const size = media.take(4).size; const uncached = media.every(attachment => attachment.get('type') === 'unknown'); @@ -355,18 +339,16 @@ class MediaGallery extends React.PureComponent { const computedClass = classNames('media-gallery', { 'full-width': fullwidth }); - if (this.isStandaloneEligible() && width) { - style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); - } else if (width) { - style.height = width / (16/9); + if (this.isStandaloneEligible()) { // TODO: cropImages setting + style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`; } else { - return (
    ); + style.aspectRatio = '16 / 9'; } if (this.isStandaloneEligible()) { - children = ; + children = ; } else { - children = media.take(4).map((attachment, i) => ); + children = media.take(4).map((attachment, i) => ); } if (uncached) { @@ -402,3 +384,5 @@ class MediaGallery extends React.PureComponent { } } + +export default injectIntl(MediaGallery); diff --git a/app/javascript/flavours/glitch/components/missing_indicator.js b/app/javascript/flavours/glitch/components/missing_indicator.js deleted file mode 100644 index 08e39c2367..0000000000 --- a/app/javascript/flavours/glitch/components/missing_indicator.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import illustration from 'flavours/glitch/images/elephant_ui_disappointed.svg'; -import classNames from 'classnames'; -import { Helmet } from 'react-helmet'; - -const MissingIndicator = ({ fullPage }) => ( -
    -
    - -
    - -
    - - -
    - - - - -
    -); - -MissingIndicator.propTypes = { - fullPage: PropTypes.bool, -}; - -export default MissingIndicator; diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.jsx similarity index 96% rename from app/javascript/flavours/glitch/components/modal_root.js rename to app/javascript/flavours/glitch/components/modal_root.jsx index 056277447a..a99c51f924 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.jsx @@ -1,10 +1,12 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import 'wicg-inert'; -import { createBrowserHistory } from 'history'; -import { multiply } from 'color-blend'; +import { PureComponent } from 'react'; + +import 'wicg-inert'; +import { multiply } from 'color-blend'; +import { createBrowserHistory } from 'history'; + +export default class ModalRoot extends PureComponent { -export default class ModalRoot extends React.PureComponent { static contextTypes = { router: PropTypes.object, }; @@ -28,7 +30,7 @@ export default class ModalRoot extends React.PureComponent { && !!this.props.children && !this.props.noEsc) { this.props.onClose(); } - } + }; handleKeyDown = (e) => { if (e.key === 'Tab') { @@ -49,7 +51,7 @@ export default class ModalRoot extends React.PureComponent { e.preventDefault(); } } - } + }; componentDidMount () { window.addEventListener('keyup', this.handleKeyUp, false); @@ -61,7 +63,7 @@ export default class ModalRoot extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!!nextProps.children && !this.props.children) { this.activeElement = document.activeElement; @@ -125,11 +127,11 @@ export default class ModalRoot extends React.PureComponent { getSiblings = () => { return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); - } + }; setRef = ref => { this.node = ref; - } + }; render () { const { children, onClose } = this.props; diff --git a/app/javascript/flavours/glitch/components/navigation_portal.js b/app/javascript/flavours/glitch/components/navigation_portal.jsx similarity index 79% rename from app/javascript/flavours/glitch/components/navigation_portal.js rename to app/javascript/flavours/glitch/components/navigation_portal.jsx index 90afa1da0b..e142a3ec60 100644 --- a/app/javascript/flavours/glitch/components/navigation_portal.js +++ b/app/javascript/flavours/glitch/components/navigation_portal.jsx @@ -1,22 +1,21 @@ -import React from 'react'; +import { PureComponent } from 'react'; + import { Switch, Route, withRouter } from 'react-router-dom'; -import { showTrends } from 'flavours/glitch/initial_state'; -import Trends from 'flavours/glitch/features/getting_started/containers/trends_container'; + import AccountNavigation from 'flavours/glitch/features/account/navigation'; +import Trends from 'flavours/glitch/features/getting_started/containers/trends_container'; +import { showTrends } from 'flavours/glitch/initial_state'; const DefaultNavigation = () => ( - <> - {showTrends && ( - <> -
    - - - )} - + showTrends ? ( + <> +
    + + + ) : null ); -export default @withRouter -class NavigationPortal extends React.PureComponent { +class NavigationPortal extends PureComponent { render () { return ( @@ -33,3 +32,5 @@ class NavigationPortal extends React.PureComponent { } } + +export default withRouter(NavigationPortal); diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.js b/app/javascript/flavours/glitch/components/not_signed_in_indicator.js deleted file mode 100644 index b440c6be2f..0000000000 --- a/app/javascript/flavours/glitch/components/not_signed_in_indicator.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -const NotSignedInIndicator = () => ( -
    -
    - -
    -
    -); - -export default NotSignedInIndicator; diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx new file mode 100644 index 0000000000..d0eedc6412 --- /dev/null +++ b/app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; + +import { FormattedMessage } from 'react-intl'; + +export const NotSignedInIndicator: React.FC = () => ( +
    +
    + +
    +
    +); diff --git a/app/javascript/flavours/glitch/components/notification_purge_buttons.js b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx similarity index 74% rename from app/javascript/flavours/glitch/components/notification_purge_buttons.js rename to app/javascript/flavours/glitch/components/notification_purge_buttons.jsx index 3c7d67109d..dfc7ac5a8a 100644 --- a/app/javascript/flavours/glitch/components/notification_purge_buttons.js +++ b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx @@ -6,11 +6,15 @@ // Package imports // -import React from 'react'; import PropTypes from 'prop-types'; + import { defineMessages, injectIntl } from 'react-intl'; + +import classNames from 'classnames'; + import ImmutablePureComponent from 'react-immutable-pure-component'; -import Icon from 'flavours/glitch/components/icon'; + +import { Icon } from 'flavours/glitch/components/icon'; const messages = defineMessages({ btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' }, @@ -19,7 +23,6 @@ const messages = defineMessages({ btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' }, }); -export default @injectIntl class NotificationPurgeButtons extends ImmutablePureComponent { static propTypes = { @@ -37,19 +40,19 @@ class NotificationPurgeButtons extends ImmutablePureComponent { //className='active' return (
    - - - -
    @@ -57,3 +60,5 @@ class NotificationPurgeButtons extends ImmutablePureComponent { } } + +export default injectIntl(NotificationPurgeButtons); diff --git a/app/javascript/flavours/glitch/components/permalink.js b/app/javascript/flavours/glitch/components/permalink.jsx similarity index 75% rename from app/javascript/flavours/glitch/components/permalink.js rename to app/javascript/flavours/glitch/components/permalink.jsx index 718b021150..fa33ce066a 100644 --- a/app/javascript/flavours/glitch/components/permalink.js +++ b/app/javascript/flavours/glitch/components/permalink.jsx @@ -1,7 +1,7 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; -export default class Permalink extends React.PureComponent { +export default class Permalink extends PureComponent { static contextTypes = { router: PropTypes.object, @@ -24,12 +24,10 @@ export default class Permalink extends React.PureComponent { if (this.context.router) { e.preventDefault(); - let state = {...this.context.router.history.location.state}; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(this.props.to, state); + this.context.router.history.push(this.props.to); } } - } + }; render () { const { diff --git a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.js b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.js deleted file mode 100644 index 01dce0a383..0000000000 --- a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Icon from 'flavours/glitch/components/icon'; -import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; -import { connect } from 'react-redux'; -import { debounce } from 'lodash'; -import { FormattedMessage } from 'react-intl'; - -export default @connect() -class PictureInPicturePlaceholder extends React.PureComponent { - - static propTypes = { - width: PropTypes.number, - dispatch: PropTypes.func.isRequired, - }; - - state = { - width: this.props.width, - height: this.props.width && (this.props.width / (16/9)), - }; - - handleClick = () => { - const { dispatch } = this.props; - dispatch(removePictureInPicture()); - } - - setRef = c => { - this.node = c; - - if (this.node) { - this._setDimensions(); - } - } - - _setDimensions () { - const width = this.node.offsetWidth; - const height = width / (16/9); - - this.setState({ width, height }); - } - - componentDidMount () { - window.addEventListener('resize', this.handleResize, { passive: true }); - } - - componentWillUnmount () { - window.removeEventListener('resize', this.handleResize); - } - - handleResize = debounce(() => { - if (this.node) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - - render () { - const { height } = this.state; - - return ( -
    - - -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx new file mode 100644 index 0000000000..1a290c91de --- /dev/null +++ b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { connect } from 'react-redux'; + +import { removePictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; +import { Icon } from 'flavours/glitch/components/icon'; + +class PictureInPicturePlaceholder extends PureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + }; + + handleClick = () => { + const { dispatch } = this.props; + dispatch(removePictureInPicture()); + }; + + render () { + return ( +
    + + +
    + ); + } + +} + +export default connect()(PictureInPicturePlaceholder); diff --git a/app/javascript/flavours/glitch/components/poll.js b/app/javascript/flavours/glitch/components/poll.jsx similarity index 82% rename from app/javascript/flavours/glitch/components/poll.js rename to app/javascript/flavours/glitch/components/poll.jsx index da65cd2415..623d343806 100644 --- a/app/javascript/flavours/glitch/components/poll.js +++ b/app/javascript/flavours/glitch/components/poll.jsx @@ -1,15 +1,21 @@ -import React from 'react'; import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; -import Motion from 'flavours/glitch/features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; + import escapeTextContentForBrowser from 'escape-html'; +import spring from 'react-motion/lib/spring'; + +import { Icon } from 'flavours/glitch/components/icon'; import emojify from 'flavours/glitch/features/emoji/emoji'; -import RelativeTimestamp from './relative_timestamp'; -import Icon from 'flavours/glitch/components/icon'; +import Motion from 'flavours/glitch/features/ui/util/optional_motion'; + +import { RelativeTimestamp } from './relative_timestamp'; + const messages = defineMessages({ closed: { @@ -31,7 +37,6 @@ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { return obj; }, {}); -export default @injectIntl class Poll extends ImmutablePureComponent { static contextTypes = { @@ -40,6 +45,7 @@ class Poll extends ImmutablePureComponent { static propTypes = { poll: ImmutablePropTypes.map, + lang: PropTypes.string, intl: PropTypes.object.isRequired, disabled: PropTypes.bool, refresh: PropTypes.func, @@ -52,9 +58,9 @@ class Poll extends ImmutablePureComponent { }; static getDerivedStateFromProps (props, state) { - const { poll, intl } = props; + const { poll } = props; const expires_at = poll.get('expires_at'); - const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now(); + const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now(); return (expired === state.expired) ? null : { expired }; } @@ -71,10 +77,10 @@ class Poll extends ImmutablePureComponent { } _setupTimer () { - const { poll, intl } = this.props; + const { poll } = this.props; clearTimeout(this._timer); if (!this.state.expired) { - const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now(); + const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now(); this._timer = setTimeout(() => { this.setState({ expired: true }); }, delay); @@ -95,7 +101,7 @@ class Poll extends ImmutablePureComponent { tmp[value] = true; this.setState({ selected: tmp }); } - } + }; handleOptionChange = ({ target: { value } }) => { this._toggleOption(value); @@ -107,7 +113,7 @@ class Poll extends ImmutablePureComponent { e.stopPropagation(); e.preventDefault(); } - } + }; handleVote = () => { if (this.props.disabled) { @@ -125,18 +131,24 @@ class Poll extends ImmutablePureComponent { this.props.refresh(); }; + handleReveal = () => { + this.setState({ revealed: true }); + } + renderOption (option, optionIndex, showResults) { - const { poll, disabled, intl } = this.props; + const { poll, lang, disabled, intl } = this.props; const pollVotesCount = poll.get('voters_count') || poll.get('votes_count'); const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100; const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count')); const active = !!this.state.selected[`${optionIndex}`]; const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); - let titleEmojified = option.get('title_emojified'); - if (!titleEmojified) { + const title = option.getIn(['translation', 'title']) || option.get('title'); + let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml'); + + if (!titleHtml) { const emojiMap = makeEmojiMap(poll); - titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap); + titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap); } return ( @@ -154,11 +166,12 @@ class Poll extends ImmutablePureComponent { {!showResults && ( )} @@ -175,7 +188,8 @@ class Poll extends ImmutablePureComponent { {!!voted && @@ -196,14 +210,14 @@ class Poll extends ImmutablePureComponent { render () { const { poll, intl } = this.props; - const { expired } = this.state; + const { revealed, expired } = this.state; if (!poll) { return null; } const timeRemaining = expired ? intl.formatMessage(messages.closed) : ; - const showResults = poll.get('voted') || expired; + const showResults = poll.get('voted') || revealed || expired; const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); let votesCount = null; @@ -222,12 +236,15 @@ class Poll extends ImmutablePureComponent {
    {!showResults && } - {showResults && !this.props.disabled && · } + {!showResults && <> · } + {showResults && !this.props.disabled && <> · } {votesCount} - {poll.get('expires_at') && · {timeRemaining}} + {poll.get('expires_at') && <> · {timeRemaining}}
    ); } } + +export default injectIntl(Poll); diff --git a/app/javascript/flavours/glitch/components/radio_button.js b/app/javascript/flavours/glitch/components/radio_button.js deleted file mode 100644 index 0496fa2868..0000000000 --- a/app/javascript/flavours/glitch/components/radio_button.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -export default class RadioButton extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - checked: PropTypes.bool, - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - label: PropTypes.node.isRequired, - }; - - render () { - const { name, value, checked, onChange, label } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/radio_button.tsx b/app/javascript/flavours/glitch/components/radio_button.tsx new file mode 100644 index 0000000000..fbd7859d80 --- /dev/null +++ b/app/javascript/flavours/glitch/components/radio_button.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; + +import classNames from 'classnames'; + +interface Props { + value: string; + checked: boolean; + name: string; + onChange: (event: React.ChangeEvent) => void; + label: React.ReactNode; +} + +export const RadioButton: React.FC = ({ + name, + value, + checked, + onChange, + label, +}) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/regeneration_indicator.js b/app/javascript/flavours/glitch/components/regeneration_indicator.jsx similarity index 96% rename from app/javascript/flavours/glitch/components/regeneration_indicator.js rename to app/javascript/flavours/glitch/components/regeneration_indicator.jsx index 68ce09df9b..78844f389d 100644 --- a/app/javascript/flavours/glitch/components/regeneration_indicator.js +++ b/app/javascript/flavours/glitch/components/regeneration_indicator.jsx @@ -1,5 +1,5 @@ -import React from 'react'; import { FormattedMessage } from 'react-intl'; + import illustration from 'flavours/glitch/images/elephant_ui_working.svg'; const RegenerationIndicator = () => ( diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.js b/app/javascript/flavours/glitch/components/relative_timestamp.js deleted file mode 100644 index 5124803392..0000000000 --- a/app/javascript/flavours/glitch/components/relative_timestamp.js +++ /dev/null @@ -1,199 +0,0 @@ -import React from 'react'; -import { injectIntl, defineMessages } from 'react-intl'; -import PropTypes from 'prop-types'; - -const messages = defineMessages({ - today: { id: 'relative_time.today', defaultMessage: 'today' }, - just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, - just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' }, - seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, - seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' }, - minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, - minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' }, - hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, - hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' }, - days: { id: 'relative_time.days', defaultMessage: '{number}d' }, - days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' }, - moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, - seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' }, - minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' }, - hours_remaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left' }, - days_remaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left' }, -}); - -const dateFormatOptions = { - hour12: false, - year: 'numeric', - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', -}; - -const shortDateFormatOptions = { - month: 'short', - day: 'numeric', -}; - -const SECOND = 1000; -const MINUTE = 1000 * 60; -const HOUR = 1000 * 60 * 60; -const DAY = 1000 * 60 * 60 * 24; - -const MAX_DELAY = 2147483647; - -const selectUnits = delta => { - const absDelta = Math.abs(delta); - - if (absDelta < MINUTE) { - return 'second'; - } else if (absDelta < HOUR) { - return 'minute'; - } else if (absDelta < DAY) { - return 'hour'; - } - - return 'day'; -}; - -const getUnitDelay = units => { - switch (units) { - case 'second': - return SECOND; - case 'minute': - return MINUTE; - case 'hour': - return HOUR; - case 'day': - return DAY; - default: - return MAX_DELAY; - } -}; - -export const timeAgoString = (intl, date, now, year, timeGiven, short) => { - const delta = now - date.getTime(); - - let relativeTime; - - if (delta < DAY && !timeGiven) { - relativeTime = intl.formatMessage(messages.today); - } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full); - } else if (delta < 7 * DAY) { - if (delta < MINUTE) { - relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) }); - } else if (delta < HOUR) { - relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) }); - } else if (delta < DAY) { - relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) }); - } else { - relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) }); - } - } else if (date.getFullYear() === year) { - relativeTime = intl.formatDate(date, shortDateFormatOptions); - } else { - relativeTime = intl.formatDate(date, { ...shortDateFormatOptions, year: 'numeric' }); - } - - return relativeTime; -}; - -const timeRemainingString = (intl, date, now, timeGiven = true) => { - const delta = date.getTime() - now; - - let relativeTime; - - if (delta < DAY && !timeGiven) { - relativeTime = intl.formatMessage(messages.today); - } else if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(messages.moments_remaining); - } else if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); - } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes_remaining, { number: Math.floor(delta / MINUTE) }); - } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours_remaining, { number: Math.floor(delta / HOUR) }); - } else { - relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) }); - } - - return relativeTime; -}; - -export default @injectIntl -class RelativeTimestamp extends React.Component { - - static propTypes = { - intl: PropTypes.object.isRequired, - timestamp: PropTypes.string.isRequired, - year: PropTypes.number.isRequired, - futureDate: PropTypes.bool, - short: PropTypes.bool, - }; - - state = { - now: this.props.intl.now(), - }; - - static defaultProps = { - year: (new Date()).getFullYear(), - short: true, - }; - - shouldComponentUpdate (nextProps, nextState) { - // As of right now the locale doesn't change without a new page load, - // but we might as well check in case that ever changes. - return this.props.timestamp !== nextProps.timestamp || - this.props.intl.locale !== nextProps.intl.locale || - this.state.now !== nextState.now; - } - - componentWillReceiveProps (nextProps) { - if (this.props.timestamp !== nextProps.timestamp) { - this.setState({ now: this.props.intl.now() }); - } - } - - componentDidMount () { - this._scheduleNextUpdate(this.props, this.state); - } - - componentWillUpdate (nextProps, nextState) { - this._scheduleNextUpdate(nextProps, nextState); - } - - componentWillUnmount () { - clearTimeout(this._timer); - } - - _scheduleNextUpdate (props, state) { - clearTimeout(this._timer); - - const { timestamp } = props; - const delta = (new Date(timestamp)).getTime() - state.now; - const unitDelay = getUnitDelay(selectUnits(delta)); - const unitRemainder = Math.abs(delta % unitDelay); - const updateInterval = 1000 * 10; - const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder); - - this._timer = setTimeout(() => { - this.setState({ now: this.props.intl.now() }); - }, delay); - } - - render () { - const { timestamp, intl, year, futureDate, short } = this.props; - - const timeGiven = timestamp.includes('T'); - const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short); - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/relative_timestamp.tsx b/app/javascript/flavours/glitch/components/relative_timestamp.tsx new file mode 100644 index 0000000000..e4a8437d0e --- /dev/null +++ b/app/javascript/flavours/glitch/components/relative_timestamp.tsx @@ -0,0 +1,282 @@ +import { Component } from 'react'; + +import type { IntlShape } from 'react-intl'; +import { injectIntl, defineMessages } from 'react-intl'; + +const messages = defineMessages({ + today: { id: 'relative_time.today', defaultMessage: 'today' }, + just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, + just_now_full: { + id: 'relative_time.full.just_now', + defaultMessage: 'just now', + }, + seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, + seconds_full: { + id: 'relative_time.full.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} ago', + }, + minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, + minutes_full: { + id: 'relative_time.full.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago', + }, + hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, + hours_full: { + id: 'relative_time.full.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} ago', + }, + days: { id: 'relative_time.days', defaultMessage: '{number}d' }, + days_full: { + id: 'relative_time.full.days', + defaultMessage: '{number, plural, one {# day} other {# days}} ago', + }, + moments_remaining: { + id: 'time_remaining.moments', + defaultMessage: 'Moments remaining', + }, + seconds_remaining: { + id: 'time_remaining.seconds', + defaultMessage: '{number, plural, one {# second} other {# seconds}} left', + }, + minutes_remaining: { + id: 'time_remaining.minutes', + defaultMessage: '{number, plural, one {# minute} other {# minutes}} left', + }, + hours_remaining: { + id: 'time_remaining.hours', + defaultMessage: '{number, plural, one {# hour} other {# hours}} left', + }, + days_remaining: { + id: 'time_remaining.days', + defaultMessage: '{number, plural, one {# day} other {# days}} left', + }, +}); + +const dateFormatOptions = { + hour12: false, + year: 'numeric', + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', +} as const; + +const shortDateFormatOptions = { + month: 'short', + day: 'numeric', +} as const; + +const SECOND = 1000; +const MINUTE = 1000 * 60; +const HOUR = 1000 * 60 * 60; +const DAY = 1000 * 60 * 60 * 24; + +const MAX_DELAY = 2147483647; + +const selectUnits = (delta: number) => { + const absDelta = Math.abs(delta); + + if (absDelta < MINUTE) { + return 'second'; + } else if (absDelta < HOUR) { + return 'minute'; + } else if (absDelta < DAY) { + return 'hour'; + } + + return 'day'; +}; + +const getUnitDelay = (units: string) => { + switch (units) { + case 'second': + return SECOND; + case 'minute': + return MINUTE; + case 'hour': + return HOUR; + case 'day': + return DAY; + default: + return MAX_DELAY; + } +}; + +export const timeAgoString = ( + intl: IntlShape, + date: Date, + now: number, + year: number, + timeGiven: boolean, + short?: boolean +) => { + const delta = now - date.getTime(); + + let relativeTime; + + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { + relativeTime = intl.formatMessage( + short ? messages.just_now : messages.just_now_full + ); + } else if (delta < 7 * DAY) { + if (delta < MINUTE) { + relativeTime = intl.formatMessage( + short ? messages.seconds : messages.seconds_full, + { number: Math.floor(delta / SECOND) } + ); + } else if (delta < HOUR) { + relativeTime = intl.formatMessage( + short ? messages.minutes : messages.minutes_full, + { number: Math.floor(delta / MINUTE) } + ); + } else if (delta < DAY) { + relativeTime = intl.formatMessage( + short ? messages.hours : messages.hours_full, + { number: Math.floor(delta / HOUR) } + ); + } else { + relativeTime = intl.formatMessage( + short ? messages.days : messages.days_full, + { number: Math.floor(delta / DAY) } + ); + } + } else if (date.getFullYear() === year) { + relativeTime = intl.formatDate(date, shortDateFormatOptions); + } else { + relativeTime = intl.formatDate(date, { + ...shortDateFormatOptions, + year: 'numeric', + }); + } + + return relativeTime; +}; + +const timeRemainingString = ( + intl: IntlShape, + date: Date, + now: number, + timeGiven = true +) => { + const delta = date.getTime() - now; + + let relativeTime; + + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { + relativeTime = intl.formatMessage(messages.moments_remaining); + } else if (delta < MINUTE) { + relativeTime = intl.formatMessage(messages.seconds_remaining, { + number: Math.floor(delta / SECOND), + }); + } else if (delta < HOUR) { + relativeTime = intl.formatMessage(messages.minutes_remaining, { + number: Math.floor(delta / MINUTE), + }); + } else if (delta < DAY) { + relativeTime = intl.formatMessage(messages.hours_remaining, { + number: Math.floor(delta / HOUR), + }); + } else { + relativeTime = intl.formatMessage(messages.days_remaining, { + number: Math.floor(delta / DAY), + }); + } + + return relativeTime; +}; + +interface Props { + intl: IntlShape; + timestamp: string; + year: number; + futureDate?: boolean; + short?: boolean; +} +interface States { + now: number; +} +class RelativeTimestamp extends Component { + state = { + now: Date.now(), + }; + + static defaultProps = { + year: new Date().getFullYear(), + short: true, + }; + + _timer: number | undefined; + + shouldComponentUpdate(nextProps: Props, nextState: States) { + // As of right now the locale doesn't change without a new page load, + // but we might as well check in case that ever changes. + return ( + this.props.timestamp !== nextProps.timestamp || + this.props.intl.locale !== nextProps.intl.locale || + this.state.now !== nextState.now + ); + } + + UNSAFE_componentWillReceiveProps(nextProps: Props) { + if (this.props.timestamp !== nextProps.timestamp) { + this.setState({ now: Date.now() }); + } + } + + componentDidMount() { + this._scheduleNextUpdate(this.props, this.state); + } + + UNSAFE_componentWillUpdate(nextProps: Props, nextState: States) { + this._scheduleNextUpdate(nextProps, nextState); + } + + componentWillUnmount() { + window.clearTimeout(this._timer); + } + + _scheduleNextUpdate(props: Props, state: States) { + window.clearTimeout(this._timer); + + const { timestamp } = props; + const delta = new Date(timestamp).getTime() - state.now; + const unitDelay = getUnitDelay(selectUnits(delta)); + const unitRemainder = Math.abs(delta % unitDelay); + const updateInterval = 1000 * 10; + const delay = + delta < 0 + ? Math.max(updateInterval, unitDelay - unitRemainder) + : Math.max(updateInterval, unitRemainder); + + this._timer = window.setTimeout(() => { + this.setState({ now: Date.now() }); + }, delay); + } + + render() { + const { timestamp, intl, year, futureDate, short } = this.props; + + const timeGiven = timestamp.includes('T'); + const date = new Date(timestamp); + const relativeTime = futureDate + ? timeRemainingString(intl, date, this.state.now, timeGiven) + : timeAgoString(intl, date, this.state.now, year, timeGiven, short); + + return ( + + ); + } +} + +const RelativeTimestampWithIntl = injectIntl(RelativeTimestamp); + +export { RelativeTimestampWithIntl as RelativeTimestamp }; diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.jsx similarity index 83% rename from app/javascript/flavours/glitch/components/scrollable_list.js rename to app/javascript/flavours/glitch/components/scrollable_list.jsx index 8eb2b66d43..8d18c2081b 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.jsx @@ -1,26 +1,34 @@ -import React, { PureComponent } from 'react'; -import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import PropTypes from 'prop-types'; -import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; -import LoadMore from './load_more'; -import LoadPending from './load_pending'; -import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/intersection_observer_wrapper'; -import { throttle } from 'lodash'; -import { List as ImmutableList } from 'immutable'; +import { Children, cloneElement, PureComponent } from 'react'; + import classNames from 'classnames'; -import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; -import LoadingIndicator from './loading_indicator'; + +import { List as ImmutableList } from 'immutable'; import { connect } from 'react-redux'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import { throttle } from 'lodash'; + +import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; +import ScrollContainer from 'flavours/glitch/containers/scroll_container'; +import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/intersection_observer_wrapper'; + +import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; + +import { LoadMore } from './load_more'; +import { LoadPending } from './load_pending'; +import { LoadingIndicator } from './loading_indicator'; + const MOUSE_IDLE_DELAY = 300; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + const mapStateToProps = (state, { scrollKey }) => { return { preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']), }; }; -export default @connect(mapStateToProps, null, null, { forwardRef: true }) class ScrollableList extends PureComponent { static contextTypes = { @@ -91,15 +99,19 @@ class ScrollableList extends PureComponent { lastScrollWasSynthetic = false; scrollToTopOnMouseIdle = false; + _getScrollingElement = () => { + if (this.props.bindToDocument) { + return (document.scrollingElement || document.body); + } else { + return this.node; + } + }; + setScrollTop = newScrollTop => { if (this.getScrollTop() !== newScrollTop) { this.lastScrollWasSynthetic = true; - if (this.props.bindToDocument) { - document.scrollingElement.scrollTop = newScrollTop; - } else { - this.node.scrollTop = newScrollTop; - } + this._getScrollingElement().scrollTop = newScrollTop; } }; @@ -107,6 +119,7 @@ class ScrollableList extends PureComponent { if (this.mouseIdleTimer === null) { return; } + clearTimeout(this.mouseIdleTimer); this.mouseIdleTimer = null; }; @@ -114,13 +127,13 @@ class ScrollableList extends PureComponent { handleMouseMove = throttle(() => { // As long as the mouse keeps moving, clear and restart the idle timer. this.clearMouseIdleTimer(); - this.mouseIdleTimer = - setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); + this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); if (!this.mouseMovedRecently && this.getScrollTop() === 0) { // Only set if we just started moving and are scrolled to the top. this.scrollToTopOnMouseIdle = true; } + // Save setting this flag for last, so we can do the comparison above. this.mouseMovedRecently = true; }, MOUSE_IDLE_DELAY / 2); @@ -135,13 +148,15 @@ class ScrollableList extends PureComponent { if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) { this.setScrollTop(0); } + this.mouseMovedRecently = false; this.scrollToTopOnMouseIdle = false; - } + }; componentDidMount () { this.attachScrollListener(); this.attachIntersectionObserver(); + attachFullscreenListener(this.onFullScreenChange); // Handle initial scroll position @@ -154,33 +169,29 @@ class ScrollableList extends PureComponent { } else { return null; } - } + }; getScrollTop = () => { - return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop; - } + return this._getScrollingElement().scrollTop; + }; getScrollHeight = () => { - return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight; - } + return this._getScrollingElement().scrollHeight; + }; getClientHeight = () => { - return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight; - } + return this._getScrollingElement().clientHeight; + }; updateScrollBottom = (snapshot) => { const newScrollTop = this.getScrollHeight() - snapshot; this.setScrollTop(newScrollTop); - } + }; - cacheMediaWidth = (width) => { - if (width && this.state.cachedMediaWidth != width) this.setState({ cachedMediaWidth: width }); - } - - getSnapshotBeforeUpdate (prevProps, prevState) { - const someItemInserted = React.Children.count(prevProps.children) > 0 && - React.Children.count(prevProps.children) < React.Children.count(this.props.children) && + getSnapshotBeforeUpdate (prevProps) { + const someItemInserted = Children.count(prevProps.children) > 0 && + Children.count(prevProps.children) < Children.count(this.props.children) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); @@ -199,22 +210,32 @@ class ScrollableList extends PureComponent { } } + cacheMediaWidth = (width) => { + if (width && this.state.cachedMediaWidth !== width) { + this.setState({ cachedMediaWidth: width }); + } + }; + componentWillUnmount () { this.clearMouseIdleTimer(); this.detachScrollListener(); this.detachIntersectionObserver(); + detachFullscreenListener(this.onFullScreenChange); } onFullScreenChange = () => { this.setState({ fullscreen: isFullscreen() }); - } + }; attachIntersectionObserver () { - this.intersectionObserverWrapper.connect({ + let nodeOptions = { root: this.node, rootMargin: '300% 0px', - }); + }; + + this.intersectionObserverWrapper + .connect(this.props.bindToDocument ? {} : nodeOptions); } detachIntersectionObserver () { @@ -224,20 +245,20 @@ class ScrollableList extends PureComponent { attachScrollListener () { if (this.props.bindToDocument) { document.addEventListener('scroll', this.handleScroll); - document.addEventListener('wheel', this.handleWheel); + document.addEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel); + this.node.addEventListener('wheel', this.handleWheel, listenerOptions); } } detachScrollListener () { if (this.props.bindToDocument) { document.removeEventListener('scroll', this.handleScroll); - document.removeEventListener('wheel', this.handleWheel); + document.removeEventListener('wheel', this.handleWheel, listenerOptions); } else { this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel); + this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); } } @@ -256,12 +277,12 @@ class ScrollableList extends PureComponent { setRef = (c) => { this.node = c; - } + }; handleLoadMore = e => { e.preventDefault(); this.props.onLoadMore(); - } + }; handleLoadPending = e => { e.preventDefault(); @@ -273,12 +294,12 @@ class ScrollableList extends PureComponent { this.clearMouseIdleTimer(); this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY); this.mouseMovedRecently = true; - } + }; render () { const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; - const childrenCount = React.Children.count(children); + const childrenCount = Children.count(children); const loadMore = (hasMore && onLoadMore) ? : null; const loadPending = (numPending > 0) ? : null; @@ -304,7 +325,7 @@ class ScrollableList extends PureComponent { {loadPending} - {React.Children.map(this.props.children, (child, index) => ( + {Children.map(this.props.children, (child, index) => ( - {React.cloneElement(child, { + {cloneElement(child, { getScrollPosition: this.getScrollPosition, updateScrollBottom: this.updateScrollBottom, cachedMediaWidth: this.state.cachedMediaWidth, @@ -352,3 +373,5 @@ class ScrollableList extends PureComponent { } } + +export default connect(mapStateToProps, null, null, { forwardRef: true })(ScrollableList); diff --git a/app/javascript/flavours/glitch/components/server_banner.js b/app/javascript/flavours/glitch/components/server_banner.jsx similarity index 87% rename from app/javascript/flavours/glitch/components/server_banner.js rename to app/javascript/flavours/glitch/components/server_banner.jsx index 36e0ff2381..4809df1ddc 100644 --- a/app/javascript/flavours/glitch/components/server_banner.js +++ b/app/javascript/flavours/glitch/components/server_banner.jsx @@ -1,14 +1,18 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import { PureComponent } from 'react'; + import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; + +import { Link } from 'react-router-dom'; + import { connect } from 'react-redux'; + import { fetchServer } from 'flavours/glitch/actions/server'; +import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; 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 { domain } from 'flavours/glitch/initial_state'; -import Image from 'flavours/glitch/components/image'; -import { Link } from 'react-router-dom'; const messages = defineMessages({ aboutActiveUsers: { id: 'server_banner.about_active_users', defaultMessage: 'People using this server during the last 30 days (Monthly Active Users)' }, @@ -18,9 +22,7 @@ const mapStateToProps = state => ({ server: state.getIn(['server', 'server']), }); -export default @connect(mapStateToProps) -@injectIntl -class ServerBanner extends React.PureComponent { +class ServerBanner extends PureComponent { static propTypes = { server: PropTypes.object, @@ -43,7 +45,7 @@ class ServerBanner extends React.PureComponent { {domain}, mastodon:
    Mastodon }} />
    - +
    {isLoading ? ( @@ -91,3 +93,5 @@ class ServerBanner extends React.PureComponent { } } + +export default connect(mapStateToProps)(injectIntl(ServerBanner)); diff --git a/app/javascript/flavours/glitch/components/server_hero_image.tsx b/app/javascript/flavours/glitch/components/server_hero_image.tsx new file mode 100644 index 0000000000..be05059e49 --- /dev/null +++ b/app/javascript/flavours/glitch/components/server_hero_image.tsx @@ -0,0 +1,36 @@ +import { useCallback, useState } from 'react'; +import * as React from 'react'; + +import classNames from 'classnames'; + +import { Blurhash } from './blurhash'; + +interface Props { + src: string; + srcSet?: string; + blurhash?: string; + className?: string; +} + +export const ServerHeroImage: React.FC = ({ + src, + srcSet, + blurhash, + className, +}) => { + const [loaded, setLoaded] = useState(false); + + const handleLoad = useCallback(() => { + setLoaded(true); + }, [setLoaded]); + + return ( +
    + {blurhash && } + +
    + ); +}; diff --git a/app/javascript/flavours/glitch/components/setting_text.js b/app/javascript/flavours/glitch/components/setting_text.jsx similarity index 88% rename from app/javascript/flavours/glitch/components/setting_text.js rename to app/javascript/flavours/glitch/components/setting_text.jsx index 2c1b70bc34..79d4bf8ea3 100644 --- a/app/javascript/flavours/glitch/components/setting_text.js +++ b/app/javascript/flavours/glitch/components/setting_text.jsx @@ -1,8 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import ImmutablePropTypes from 'react-immutable-proptypes'; -export default class SettingText extends React.PureComponent { +export default class SettingText extends PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, @@ -13,7 +14,7 @@ export default class SettingText extends React.PureComponent { handleChange = (e) => { this.props.onChange(this.props.settingPath, e.target.value); - } + }; render () { const { settings, settingPath, label } = this.props; diff --git a/app/javascript/flavours/glitch/components/short_number.js b/app/javascript/flavours/glitch/components/short_number.jsx similarity index 94% rename from app/javascript/flavours/glitch/components/short_number.js rename to app/javascript/flavours/glitch/components/short_number.jsx index 535c17727d..0ddd26e783 100644 --- a/app/javascript/flavours/glitch/components/short_number.js +++ b/app/javascript/flavours/glitch/components/short_number.jsx @@ -1,7 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; +import { memo } from 'react'; + import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; // @ts-check /** @@ -24,7 +26,6 @@ import { FormattedMessage, FormattedNumber } from 'react-intl'; /** * Component that renders short big number to a shorter version - * * @param {ShortNumberProps} param0 Props for the component * @returns {JSX.Element} Rendered number */ @@ -32,17 +33,14 @@ function ShortNumber({ value, renderer, children }) { const shortNumber = toShortNumber(value); const [, division] = shortNumber; - // eslint-disable-next-line eqeqeq 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.'); } - // eslint-disable-next-line eqeqeq const customRenderer = children != null ? children : renderer; const displayNumber = ; - // eslint-disable-next-line eqeqeq return customRenderer != null ? customRenderer(displayNumber, pluralReady(value, division)) : displayNumber; @@ -61,7 +59,6 @@ ShortNumber.propTypes = { /** * 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 */ @@ -114,4 +111,4 @@ ShortNumberCounter.propTypes = { value: PropTypes.arrayOf(PropTypes.number), }; -export default React.memo(ShortNumber); +export default memo(ShortNumber); diff --git a/app/javascript/flavours/glitch/components/skeleton.js b/app/javascript/flavours/glitch/components/skeleton.js deleted file mode 100644 index 6a17ffb261..0000000000 --- a/app/javascript/flavours/glitch/components/skeleton.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Skeleton = ({ width, height }) => ; - -Skeleton.propTypes = { - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), -}; - -export default Skeleton; diff --git a/app/javascript/flavours/glitch/components/skeleton.tsx b/app/javascript/flavours/glitch/components/skeleton.tsx new file mode 100644 index 0000000000..30ff0d8525 --- /dev/null +++ b/app/javascript/flavours/glitch/components/skeleton.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; + +interface Props { + width?: number | string; + height?: number | string; +} + +export const Skeleton: React.FC = ({ width, height }) => ( + + ‌ + +); diff --git a/app/javascript/flavours/glitch/components/spoilers.js b/app/javascript/flavours/glitch/components/spoilers.js deleted file mode 100644 index 8527403c16..0000000000 --- a/app/javascript/flavours/glitch/components/spoilers.js +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; - -export default -class Spoilers extends React.PureComponent { - static propTypes = { - spoilerText: PropTypes.string, - children: PropTypes.node, - }; - - state = { - hidden: true, - } - - handleSpoilerClick = () => { - this.setState({ hidden: !this.state.hidden }); - } - - render () { - const { spoilerText, children } = this.props; - const { hidden } = this.state; - - const toggleText = hidden ? - : - ; - - return ([ -

    - {spoilerText} - {' '} - -

    , -
    - {children} -
    - ]); - } -} - diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.jsx similarity index 88% rename from app/javascript/flavours/glitch/components/status.js rename to app/javascript/flavours/glitch/components/status.jsx index 409ec0adc5..d3d432ae05 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -1,34 +1,43 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import StatusPrepend from './status_prepend'; + +import { injectIntl, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { HotKeys } from 'react-hotkeys'; + +import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; +import PollContainer from 'flavours/glitch/containers/poll_container'; +import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; +import { displayMedia } from 'flavours/glitch/initial_state'; +import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; + +import Card from '../features/status/components/card'; +import Bundle from '../features/ui/components/bundle'; +import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; + +import AttachmentList from './attachment_list'; +import StatusActionBar from './status_action_bar'; +import StatusContent from './status_content'; import StatusHeader from './status_header'; import StatusIcons from './status_icons'; -import StatusContent from './status_content'; -import StatusActionBar from './status_action_bar'; -import AttachmentList from './attachment_list'; -import Card from '../features/status/components/card'; -import { injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; -import { HotKeys } from 'react-hotkeys'; -import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; -import classNames from 'classnames'; -import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; -import PollContainer from 'flavours/glitch/containers/poll_container'; -import { displayMedia } from 'flavours/glitch/initial_state'; -import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; +import StatusPrepend from './status_prepend'; -// We use the component (and not the container) since we do not want -// to use the progress bar to show download progress -import Bundle from '../features/ui/components/bundle'; +const domParser = new DOMParser(); export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => { const displayName = status.getIn(['account', 'display_name']); + const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text'); + const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml'); + const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent; + const values = [ displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName, - status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length), + spoilerText && !expanded ? spoilerText : contentText, intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), status.getIn(['account', 'acct']), ]; @@ -54,9 +63,8 @@ export const defaultMediaVisibility = (status, settings) => { } return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); -} +}; -export default @injectIntl class Status extends ImmutablePureComponent { static contextTypes = { @@ -68,6 +76,9 @@ class Status extends ImmutablePureComponent { id: PropTypes.string, status: ImmutablePropTypes.map, account: ImmutablePropTypes.map, + previousId: PropTypes.string, + nextInReplyToId: PropTypes.string, + rootId: PropTypes.string, onReply: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, @@ -117,7 +128,7 @@ class Status extends ImmutablePureComponent { revealBehindCW: undefined, showCard: false, forceFilter: undefined, - } + }; // Avoid checking props that are functions (and whose equality will always // evaluate to false. See react-immutable-pure-component for usage. @@ -132,14 +143,17 @@ class Status extends ImmutablePureComponent { 'expanded', 'unread', 'pictureInPicture', - ] + 'previousId', + 'nextInReplyToId', + 'rootId', + ]; updateOnStates = [ 'isExpanded', 'isCollapsed', 'showMedia', 'forceFilter', - ] + ]; // If our settings have changed to disable collapsed statuses, then we // need to make sure that we uncollapse every one. We do that by watching @@ -224,7 +238,7 @@ class Status extends ImmutablePureComponent { // - The user has decided to collapse all notifications ('muted' // statuses). // - The user has decided to collapse long statuses and the status is - // over 400px (without media, or 650px with). + // over the user set value (default 400 without media, or 610px with). // - The status is a reply and the user has decided to collapse all // replies. // - The status contains media and the user has decided to collapse all @@ -251,10 +265,15 @@ class Status extends ImmutablePureComponent { // as it could cause surprising changes when receiving notifications if (settings.getIn(['content_warnings', 'shared_state']) && status.get('spoiler_text').length && !status.get('hidden')) return; + let autoCollapseHeight = parseInt(autoCollapseSettings.get('height')); + if (status.get('media_attachments').size && !muted) { + autoCollapseHeight += 210; + } + if (collapse || autoCollapseSettings.get('all') || (autoCollapseSettings.get('notifications') && muted) || - (autoCollapseSettings.get('lengthy') && node.clientHeight > ((status.get('media_attachments').size && !muted) ? 650 : 400)) || + (autoCollapseSettings.get('lengthy') && node.clientHeight > autoCollapseHeight) || (autoCollapseSettings.get('reblogs') && prepend === 'reblogged_by') || (autoCollapseSettings.get('replies') && status.get('in_reply_to_id', null) !== null) || (autoCollapseSettings.get('media') && !(status.get('spoiler_text').length) && status.get('media_attachments').size > 0) @@ -272,7 +291,7 @@ class Status extends ImmutablePureComponent { // Hack to fix timeline jumps on second rendering when auto-collapsing // or on subsequent rendering when a preview card has been fetched - getSnapshotBeforeUpdate (prevProps, prevState) { + getSnapshotBeforeUpdate() { if (!this.props.getScrollPosition) return null; const { muted, hidden, status, settings } = this.props; @@ -287,7 +306,7 @@ class Status extends ImmutablePureComponent { } } - componentDidUpdate (prevProps, prevState, snapshot) { + componentDidUpdate(prevProps, prevState, snapshot) { if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) { this.props.updateScrollBottom(snapshot.height - snapshot.top); } @@ -297,7 +316,9 @@ class Status extends ImmutablePureComponent { if (this.node && this.props.getScrollPosition) { const position = this.props.getScrollPosition(); if (position !== null && this.node.offsetTop < position.top) { - requestAnimationFrame(() => { this.props.updateScrollBottom(position.height - position.top); }); + requestAnimationFrame(() => { + this.props.updateScrollBottom(position.height - position.top); + }); } } } @@ -316,7 +337,7 @@ class Status extends ImmutablePureComponent { } else { this.setState({ isCollapsed: false }); } - } + }; setExpansion = (value) => { if (this.props.settings.getIn(['content_warnings', 'shared_state']) && this.props.status.get('hidden') === value) { @@ -327,7 +348,7 @@ class Status extends ImmutablePureComponent { if (value) { this.setCollapsed(false); } - } + }; // `parseClick()` takes a click event and responds appropriately. // If our status is collapsed, then clicking on it should uncollapse it. @@ -356,17 +377,15 @@ class Status extends ImmutablePureComponent { status.getIn(['reblog', 'id'], status.get('id')) }`; } - let state = {...router.history.location.state}; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - router.history.push(destination, state); + router.history.push(destination); } e.preventDefault(); } - } + }; handleToggleMediaVisibility = () => { this.setState({ showMedia: !this.state.showMedia }); - } + }; handleExpandedToggle = () => { if (this.props.settings.getIn(['content_warnings', 'shared_state'])) { @@ -378,12 +397,15 @@ class Status extends ImmutablePureComponent { handleOpenVideo = (options) => { const { status } = this.props; - this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); - } + const lang = status.getIn(['translation', 'language']) || status.get('language'); + this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options); + }; handleOpenMedia = (media, index) => { - this.props.onOpenMedia(this.props.status.get('id'), media, index); - } + const { status } = this.props; + const lang = status.getIn(['translation', 'language']) || status.get('language'); + this.props.onOpenMedia(status.get('id'), media, index, lang); + }; handleHotkeyOpenMedia = e => { const { status, onOpenMedia, onOpenVideo } = this.props; @@ -392,90 +414,87 @@ class Status extends ImmutablePureComponent { e.preventDefault(); if (status.get('media_attachments').size > 0) { + const lang = status.getIn(['translation', 'language']) || status.get('language'); if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 }); + onOpenVideo(statusId, status.getIn(['media_attachments', 0]), lang, { startTime: 0 }); } else { - onOpenMedia(statusId, status.get('media_attachments'), 0); + onOpenMedia(statusId, status.get('media_attachments'), 0, lang); } } - } + }; handleDeployPictureInPicture = (type, mediaProps) => { const { deployPictureInPicture, status } = this.props; deployPictureInPicture(status, type, mediaProps); - } + }; handleHotkeyReply = e => { e.preventDefault(); this.props.onReply(this.props.status, this.context.router.history); - } + }; handleHotkeyFavourite = (e) => { this.props.onFavourite(this.props.status, e); - } + }; handleHotkeyBoost = e => { this.props.onReblog(this.props.status, e); - } + }; handleHotkeyBookmark = e => { this.props.onBookmark(this.props.status, e); - } + }; handleHotkeyMention = e => { e.preventDefault(); this.props.onMention(this.props.status.get('account'), this.context.router.history); - } + }; handleHotkeyOpen = () => { - let state = {...this.context.router.history.location.state}; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; const status = this.props.status; - this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state); - } + this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); + }; handleHotkeyOpenProfile = () => { - let state = {...this.context.router.history.location.state}; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state); - } + this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); + }; handleHotkeyMoveUp = e => { this.props.onMoveUp(this.props.containerId || this.props.id, e.target.getAttribute('data-featured')); - } + }; handleHotkeyMoveDown = e => { this.props.onMoveDown(this.props.containerId || this.props.id, e.target.getAttribute('data-featured')); - } + }; - handleHotkeyCollapse = e => { + handleHotkeyCollapse = () => { if (!this.props.settings.getIn(['collapsed', 'enabled'])) return; this.setCollapsed(!this.state.isCollapsed); - } + }; handleHotkeyToggleSensitive = () => { this.handleToggleMediaVisibility(); - } + }; handleUnfilterClick = e => { this.setState({ forceFilter: false }); e.preventDefault(); - } + }; handleFilterClick = () => { this.setState({ forceFilter: true }); - } + }; handleRef = c => { this.node = c; - } + }; handleTranslate = () => { this.props.onTranslate(this.props.status); - } + }; renderLoadingMediaGallery () { return
    ; @@ -493,7 +512,6 @@ class Status extends ImmutablePureComponent { const { handleRef, parseClick, - setExpansion, setCollapsed, } = this; const { router } = this.context; @@ -512,9 +530,12 @@ class Status extends ImmutablePureComponent { unread, featured, pictureInPicture, + previousId, + nextInReplyToId, + rootId, ...other } = this.props; - const { isCollapsed, forceFilter } = this.state; + const { isCollapsed } = this.state; let background = null; let attachments = null; @@ -555,10 +576,12 @@ class Status extends ImmutablePureComponent { openMedia: this.handleHotkeyOpenMedia, }; + let prepend, rebloggedByText; + if (hidden) { return ( -
    +
    {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.get('content')}
    @@ -566,7 +589,11 @@ class Status extends ImmutablePureComponent { ); } + const connectUp = previousId && previousId === status.get('in_reply_to_id'); + const connectToRoot = rootId && rootId === status.get('in_reply_to_id'); + const connectReply = nextInReplyToId && nextInReplyToId === status.get('id'); const matchedFilters = status.get('matched_filters'); + if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) { const minHandlers = this.props.muted ? {} : { moveUp: this.handleHotkeyMoveUp, @@ -575,7 +602,7 @@ class Status extends ImmutablePureComponent { return ( -
    +
    : {matchedFilters.join(', ')}. {' '}
    -
    @@ -66,7 +68,7 @@ class Header extends ImmutablePureComponent { } else { action_buttons = (
    -
    @@ -102,3 +104,5 @@ class Header extends ImmutablePureComponent { } } + +export default injectIntl(Header); diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.jsx similarity index 79% rename from app/javascript/flavours/glitch/features/account/components/action_bar.js rename to app/javascript/flavours/glitch/features/account/components/action_bar.jsx index ce05841241..46a766925a 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.jsx @@ -1,19 +1,17 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; -import { NavLink } from 'react-router-dom'; -import { injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; -import { me, isStaff } from 'flavours/glitch/initial_state'; -import { profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; -import Icon from 'flavours/glitch/components/icon'; +import { PureComponent } from 'react'; -export default @injectIntl -class ActionBar extends React.PureComponent { +import { FormattedMessage, FormattedNumber } from 'react-intl'; + +import { NavLink } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import { Icon } from 'flavours/glitch/components/icon'; + +class ActionBar extends PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, }; isStatusesPageActive = (match, location) => { @@ -21,10 +19,10 @@ class ActionBar extends React.PureComponent { return false; } return !location.pathname.match(/\/(followers|following)\/?$/); - } + }; render () { - const { account, intl } = this.props; + const { account } = this.props; if (account.get('suspended')) { return ( @@ -32,7 +30,7 @@ class ActionBar extends React.PureComponent {
    @@ -83,3 +81,5 @@ class ActionBar extends React.PureComponent { } } + +export default ActionBar; diff --git a/app/javascript/flavours/glitch/features/account/components/featured_tags.js b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx similarity index 97% rename from app/javascript/flavours/glitch/features/account/components/featured_tags.js rename to app/javascript/flavours/glitch/features/account/components/featured_tags.jsx index d646b08b29..87e88f2fa8 100644 --- a/app/javascript/flavours/glitch/features/account/components/featured_tags.js +++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx @@ -1,8 +1,10 @@ -import React from 'react'; import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import Hashtag from 'flavours/glitch/components/hashtag'; const messages = defineMessages({ @@ -10,7 +12,6 @@ const messages = defineMessages({ empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' }, }); -export default @injectIntl class FeaturedTags extends ImmutablePureComponent { static contextTypes = { @@ -51,3 +52,5 @@ class FeaturedTags extends ImmutablePureComponent { } } + +export default injectIntl(FeaturedTags); diff --git a/app/javascript/mastodon/features/account/components/follow_request_note.js b/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx similarity index 95% rename from app/javascript/mastodon/features/account/components/follow_request_note.js rename to app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx index 300ae42660..7368ce9758 100644 --- a/app/javascript/mastodon/features/account/components/follow_request_note.js +++ b/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx @@ -1,8 +1,9 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import Icon from 'mastodon/components/icon'; + +import { Icon } from 'flavours/glitch/components/icon'; export default class FollowRequestNote extends ImmutablePureComponent { diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.jsx similarity index 94% rename from app/javascript/flavours/glitch/features/account/components/header.js rename to app/javascript/flavours/glitch/features/account/components/header.jsx index 071d00bb4c..0c440dc8a3 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.jsx @@ -1,21 +1,24 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; -import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; + import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import IconButton from 'flavours/glitch/components/icon_button'; -import Avatar from 'flavours/glitch/components/avatar'; +import { Helmet } from 'react-helmet'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Avatar } from 'flavours/glitch/components/avatar'; import Button from 'flavours/glitch/components/button'; -import { NavLink } from 'react-router-dom'; +import { Icon } from 'flavours/glitch/components/icon'; +import { IconButton } from 'flavours/glitch/components/icon_button'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; +import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; +import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; + import AccountNoteContainer from '../containers/account_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container'; -import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; -import { Helmet } from 'react-helmet'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -27,7 +30,7 @@ const messages = defineMessages({ linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' }, account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' }, mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, - direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' }, + direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, @@ -76,7 +79,6 @@ const dateFormatOptions = { minute: '2-digit', }; -export default @injectIntl class Header extends ImmutablePureComponent { static contextTypes = { @@ -109,7 +111,7 @@ class Header extends ImmutablePureComponent { openEditProfile = () => { window.open(profileLink, '_blank'); - } + }; handleMouseEnter = ({ currentTarget }) => { if (autoPlayGif) { @@ -122,7 +124,7 @@ class Header extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-original'); } - } + }; handleMouseLeave = ({ currentTarget }) => { if (autoPlayGif) { @@ -135,25 +137,24 @@ class Header extends ImmutablePureComponent { let emoji = emojis[i]; emoji.src = emoji.getAttribute('data-static'); } - } + }; handleAvatarClick = e => { if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); this.props.onOpenAvatar(); } - } + }; handleShare = () => { const { account } = this.props; navigator.share({ - text: `${titleFromAccount(account)}\n${account.get('note_plain')}`, url: account.get('url'), }).catch((e) => { if (e.name !== 'AbortError') console.error(e); }); - } + }; render () { const { account, hidden, intl, domain } = this.props; @@ -177,8 +178,7 @@ class Header extends ImmutablePureComponent { if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { info.push(); - } - else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { + } else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { info.push(); } @@ -272,16 +272,16 @@ class Header extends ImmutablePureComponent { if (account.getIn(['relationship', 'muting'])) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); } else { - menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true }); } if (account.getIn(['relationship', 'blocking'])) { menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true }); } - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); } if (signedIn && isRemote) { @@ -290,7 +290,7 @@ class Header extends ImmutablePureComponent { if (account.getIn(['relationship', 'domain_blocking'])) { menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain }); } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain }); + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true }); } } @@ -314,13 +314,18 @@ class Header extends ImmutablePureComponent { let badge; if (account.get('bot')) { - badge = (
    ); + badge = (
    ); } else if (account.get('group')) { badge = (
    ); } else { badge = null; } + let role = null; + if (account.getIn(['roles', 0])) { + role = (
    {account.getIn(['roles', 0, 'name'])}
    ); + } + return (
    {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && } @@ -337,15 +342,16 @@ class Header extends ImmutablePureComponent {
    {!suspended && (
    {!hidden && ( - + <> {actionBtn} {bellBtn} - + )} @@ -372,7 +378,7 @@ class Header extends ImmutablePureComponent { {fields.map((pair, i) => (
    - +
    {pair.get('verified_at') && }
    @@ -392,9 +398,12 @@ class Header extends ImmutablePureComponent { {titleFromAccount(account)} +
    ); } } + +export default injectIntl(Header); diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js b/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx similarity index 83% rename from app/javascript/flavours/glitch/features/account/components/profile_column_header.js rename to app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx index 17c08e375a..2dc4216bdd 100644 --- a/app/javascript/flavours/glitch/features/account/components/profile_column_header.js +++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx @@ -1,14 +1,15 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ColumnHeader from '../../../components/column_header'; +import { PureComponent } from 'react'; + import { injectIntl, defineMessages } from 'react-intl'; +import ColumnHeader from '../../../components/column_header'; + const messages = defineMessages({ profile: { id: 'column_header.profile', defaultMessage: 'Profile' }, }); -export default @injectIntl -class ProfileColumnHeader extends React.PureComponent { +class ProfileColumnHeader extends PureComponent { static propTypes = { onClick: PropTypes.func, @@ -31,3 +32,5 @@ class ProfileColumnHeader extends React.PureComponent { } } + +export default injectIntl(ProfileColumnHeader); diff --git a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js index f1d007ecb0..51d229c84b 100644 --- a/app/javascript/flavours/glitch/features/account/containers/account_note_container.js +++ b/app/javascript/flavours/glitch/features/account/containers/account_note_container.js @@ -1,5 +1,7 @@ import { connect } from 'react-redux'; + import { changeAccountNoteComment, submitAccountNote, initEditAccountNote, cancelAccountNote } from 'flavours/glitch/actions/account_notes'; + import AccountNote from '../components/account_note'; const mapStateToProps = (state, { account }) => { diff --git a/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js index 6f0b069419..bafdcba809 100644 --- a/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js +++ b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js @@ -1,7 +1,9 @@ -import { connect } from 'react-redux'; -import FeaturedTags from '../components/featured_tags'; -import { makeGetAccount } from 'flavours/glitch/selectors'; import { List as ImmutableList } from 'immutable'; +import { connect } from 'react-redux'; + +import { makeGetAccount } from 'flavours/glitch/selectors'; + +import FeaturedTags from '../components/featured_tags'; const mapStateToProps = () => { const getAccount = makeGetAccount(); diff --git a/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js b/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js index c6a3afb7e2..3b2ffbadfe 100644 --- a/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js +++ b/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js @@ -1,7 +1,9 @@ import { connect } from 'react-redux'; -import FollowRequestNote from '../components/follow_request_note'; + import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts'; +import FollowRequestNote from '../components/follow_request_note'; + const mapDispatchToProps = (dispatch, { account }) => ({ onAuthorize () { dispatch(authorizeFollowRequest(account.get('id'))); diff --git a/app/javascript/flavours/glitch/features/account/navigation.js b/app/javascript/flavours/glitch/features/account/navigation.jsx similarity index 87% rename from app/javascript/flavours/glitch/features/account/navigation.js rename to app/javascript/flavours/glitch/features/account/navigation.jsx index edae38ce5f..4be00c49f2 100644 --- a/app/javascript/flavours/glitch/features/account/navigation.js +++ b/app/javascript/flavours/glitch/features/account/navigation.jsx @@ -1,6 +1,8 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { connect } from 'react-redux'; + import FeaturedTags from 'flavours/glitch/features/account/containers/featured_tags_container'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; @@ -19,8 +21,7 @@ const mapStateToProps = (state, { match: { params: { acct } } }) => { }; }; -export default @connect(mapStateToProps) -class AccountNavigation extends React.PureComponent { +class AccountNavigation extends PureComponent { static propTypes = { match: PropTypes.shape({ @@ -50,3 +51,5 @@ class AccountNavigation extends React.PureComponent { } } + +export default connect(mapStateToProps)(AccountNavigation); diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx similarity index 93% rename from app/javascript/flavours/glitch/features/account_gallery/components/media_item.js rename to app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx index f20ee685e5..4453b557d9 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx @@ -1,12 +1,16 @@ -import Blurhash from 'flavours/glitch/components/blurhash'; -import classNames from 'classnames'; -import Icon from 'flavours/glitch/components/icon'; -import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; import PropTypes from 'prop-types'; -import React from 'react'; + +import classNames from 'classnames'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Blurhash } from 'flavours/glitch/components/blurhash'; +import { Icon } from 'flavours/glitch/components/icon'; +import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; + + + export default class MediaItem extends ImmutablePureComponent { static propTypes = { @@ -22,20 +26,20 @@ export default class MediaItem extends ImmutablePureComponent { handleImageLoad = () => { this.setState({ loaded: true }); - } + }; handleMouseEnter = e => { if (this.hoverToPlay()) { e.target.play(); } - } + }; handleMouseLeave = e => { if (this.hoverToPlay()) { e.target.pause(); e.target.currentTime = 0; } - } + }; hoverToPlay () { return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1; @@ -51,7 +55,7 @@ export default class MediaItem extends ImmutablePureComponent { this.setState({ visible: true }); } } - } + }; render () { const { attachment, displayWidth } = this.props; @@ -76,6 +80,7 @@ export default class MediaItem extends ImmutablePureComponent { {attachment.get('description')} ); @@ -95,6 +100,7 @@ export default class MediaItem extends ImmutablePureComponent { {attachment.get('description')} @@ -105,6 +111,7 @@ export default class MediaItem extends ImmutablePureComponent { className='media-gallery__item-gifv-thumbnail' aria-label={attachment.get('description')} title={attachment.get('description')} + lang={status.get('language')} role='application' src={attachment.get('url')} onMouseEnter={this.handleMouseEnter} diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.js b/app/javascript/flavours/glitch/features/account_gallery/index.jsx similarity index 86% rename from app/javascript/flavours/glitch/features/account_gallery/index.js rename to app/javascript/flavours/glitch/features/account_gallery/index.jsx index 638224bc02..2da679fc27 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.js +++ b/app/javascript/flavours/glitch/features/account_gallery/index.jsx @@ -1,21 +1,25 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; -import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; + +import { FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { getAccountGallery } from 'flavours/glitch/selectors'; -import MediaItem from './components/media_item'; -import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; -import ScrollContainer from 'flavours/glitch/containers/scroll_container'; -import LoadMore from 'flavours/glitch/components/load_more'; -import MissingIndicator from 'flavours/glitch/components/missing_indicator'; +import { connect } from 'react-redux'; + +import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; import { openModal } from 'flavours/glitch/actions/modal'; +import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; +import { LoadMore } from 'flavours/glitch/components/load_more'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import ScrollContainer from 'flavours/glitch/containers/scroll_container'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; +import HeaderContainer from 'flavours/glitch/features/account_timeline/containers/header_container'; +import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; +import Column from 'flavours/glitch/features/ui/components/column'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import { getAccountGallery } from 'flavours/glitch/selectors'; + +import MediaItem from './components/media_item'; const mapStateToProps = (state, { params: { acct, id } }) => { const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); @@ -45,7 +49,7 @@ class LoadMoreMedia extends ImmutablePureComponent { handleLoadMore = () => { this.props.onLoadMore(this.props.maxId); - } + }; render () { return ( @@ -58,7 +62,6 @@ class LoadMoreMedia extends ImmutablePureComponent { } -export default @connect(mapStateToProps) class AccountGallery extends ImmutablePureComponent { static propTypes = { @@ -72,8 +75,8 @@ class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, - multiColumn: PropTypes.bool, suspended: PropTypes.bool, + multiColumn: PropTypes.bool, }; state = { @@ -109,13 +112,13 @@ class AccountGallery extends ImmutablePureComponent { handleHeaderClick = () => { this.column.scrollTop(); - } + }; handleScrollToBottom = () => { if (this.props.hasMore) { this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined); } - } + }; handleScroll = e => { const { scrollTop, scrollHeight, clientHeight } = e.target; @@ -124,7 +127,7 @@ class AccountGallery extends ImmutablePureComponent { if (150 > offset && !this.props.isLoading) { this.handleScrollToBottom(); } - } + }; handleLoadMore = maxId => { this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId })); @@ -133,33 +136,43 @@ class AccountGallery extends ImmutablePureComponent { handleLoadOlder = e => { e.preventDefault(); this.handleScrollToBottom(); - } + }; setColumnRef = c => { this.column = c; - } + }; handleOpenMedia = attachment => { const { dispatch } = this.props; const statusId = attachment.getIn(['status', 'id']); + const lang = attachment.getIn(['status', 'language']); if (attachment.get('type') === 'video') { - dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal({ + modalType: 'VIDEO', + modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } }, + })); } else if (attachment.get('type') === 'audio') { - dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } })); + dispatch(openModal({ + modalType: 'AUDIO', + modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } }, + })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); - dispatch(openModal('MEDIA', { media, index, statusId })); + dispatch(openModal({ + modalType: 'MEDIA', + modalProps: { media, index, statusId, lang }, + })); } - } + }; handleRef = c => { if (c) { this.setState({ width: c.offsetWidth }); } - } + }; render () { const { attachments, isLoading, hasMore, isAccount, multiColumn, suspended } = this.props; @@ -167,9 +180,7 @@ class AccountGallery extends ImmutablePureComponent { if (!isAccount) { return ( - - - + ); } @@ -223,3 +234,5 @@ class AccountGallery extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(AccountGallery); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx similarity index 95% rename from app/javascript/flavours/glitch/features/account_timeline/components/header.js rename to app/javascript/flavours/glitch/features/account_timeline/components/header.jsx index 90c4c9d513..717114d5c6 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx @@ -1,11 +1,16 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import InnerHeader from 'flavours/glitch/features/account/components/header'; -import ActionBar from 'flavours/glitch/features/account/components/action_bar'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { FormattedMessage } from 'react-intl'; + import { NavLink } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import ActionBar from 'flavours/glitch/features/account/components/action_bar'; +import InnerHeader from 'flavours/glitch/features/account/components/header'; + +import MemorialNote from './memorial_note'; import MovedNote from './moved_note'; export default class Header extends ImmutablePureComponent { @@ -37,35 +42,35 @@ export default class Header extends ImmutablePureComponent { handleFollow = () => { this.props.onFollow(this.props.account); - } + }; handleBlock = () => { this.props.onBlock(this.props.account); - } + }; handleMention = () => { this.props.onMention(this.props.account, this.context.router.history); - } + }; handleDirect = () => { this.props.onDirect(this.props.account, this.context.router.history); - } + }; handleReport = () => { this.props.onReport(this.props.account); - } + }; handleReblogToggle = () => { this.props.onReblogToggle(this.props.account); - } + }; handleNotifyToggle = () => { this.props.onNotifyToggle(this.props.account); - } + }; handleMute = () => { this.props.onMute(this.props.account); - } + }; handleBlockDomain = () => { const domain = this.props.account.get('acct').split('@')[1]; @@ -73,7 +78,7 @@ export default class Header extends ImmutablePureComponent { if (!domain) return; this.props.onBlockDomain(domain); - } + }; handleUnblockDomain = () => { const domain = this.props.account.get('acct').split('@')[1]; @@ -81,31 +86,31 @@ export default class Header extends ImmutablePureComponent { if (!domain) return; this.props.onUnblockDomain(domain); - } + }; handleEndorseToggle = () => { this.props.onEndorseToggle(this.props.account); - } + }; handleAddToList = () => { this.props.onAddToList(this.props.account); - } + }; handleEditAccountNote = () => { this.props.onEditAccountNote(this.props.account); - } + }; handleChangeLanguages = () => { this.props.onChangeLanguages(this.props.account); - } + }; handleInteractionModal = () => { this.props.onInteractionModal(this.props.account); - } + }; handleOpenAvatar = () => { this.props.onOpenAvatar(this.props.account); - } + }; render () { const { account, hidden, hideTabs } = this.props; @@ -116,6 +121,7 @@ export default class Header extends ImmutablePureComponent { return (
    + {(!hidden && account.get('memorial')) && } {(!hidden && account.get('moved')) && } - +
    )} diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx similarity index 84% rename from app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js rename to app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx index ca0e60672b..5ea37a5d31 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx @@ -1,8 +1,11 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { revealAccount } from 'flavours/glitch/actions/accounts'; +import { PureComponent } from 'react'; + import { FormattedMessage } from 'react-intl'; + +import { connect } from 'react-redux'; + +import { revealAccount } from 'flavours/glitch/actions/accounts'; import Button from 'flavours/glitch/components/button'; import { domain } from 'flavours/glitch/initial_state'; @@ -14,13 +17,12 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({ }); -export default @connect(() => {}, mapDispatchToProps) -class LimitedAccountHint extends React.PureComponent { +class LimitedAccountHint extends PureComponent { static propTypes = { accountId: PropTypes.string.isRequired, reveal: PropTypes.func, - } + }; render () { const { reveal } = this.props; @@ -34,3 +36,5 @@ class LimitedAccountHint extends React.PureComponent { } } + +export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/memorial_note.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/memorial_note.jsx new file mode 100644 index 0000000000..a04808f1ca --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/components/memorial_note.jsx @@ -0,0 +1,11 @@ +import { FormattedMessage } from 'react-intl'; + +const MemorialNote = () => ( +
    +
    + +
    +
    +); + +export default MemorialNote; diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx similarity index 78% rename from app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js rename to app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx index 308407e944..2e10ea94af 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx @@ -1,11 +1,14 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; + import { FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { Icon } from 'flavours/glitch/components/icon'; + import AvatarOverlay from '../../../components/avatar_overlay'; -import DisplayName from '../../../components/display_name'; -import Icon from 'flavours/glitch/components/icon'; +import { DisplayName } from '../../../components/display_name'; export default class MovedNote extends ImmutablePureComponent { @@ -21,13 +24,11 @@ export default class MovedNote extends ImmutablePureComponent { handleAccountClick = e => { if (e.button === 0) { e.preventDefault(); - let state = {...this.context.router.history.location.state}; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/@${this.props.to.get('acct')}`, state); + this.context.router.history.push(`/@${this.props.to.get('acct')}`); } e.stopPropagation(); - } + }; render () { const { from, to } = this.props; @@ -37,7 +38,7 @@ export default class MovedNote extends ImmutablePureComponent {
    - }} /> + }} />
    diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx similarity index 63% rename from app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js rename to app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx index 25bcd01190..270865df40 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx @@ -1,7 +1,8 @@ -import React from 'react'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + import { connect } from 'react-redux'; -import { makeGetAccount, getAccountHidden } from 'flavours/glitch/selectors'; -import Header from '../components/header'; + +import { initEditAccountNote } from 'flavours/glitch/actions/account_notes'; import { followAccount, unfollowAccount, @@ -10,23 +11,24 @@ import { pinAccount, unpinAccount, } from 'flavours/glitch/actions/accounts'; +import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { mentionCompose, - directCompose + directCompose, } from 'flavours/glitch/actions/compose'; -import { initMuteModal } from 'flavours/glitch/actions/mutes'; -import { initBlockModal } from 'flavours/glitch/actions/blocks'; -import { initReport } from 'flavours/glitch/actions/reports'; -import { openModal } from 'flavours/glitch/actions/modal'; import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks'; -import { initEditAccountNote } from 'flavours/glitch/actions/account_notes'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { initReport } from 'flavours/glitch/actions/reports'; import { unfollowModal } from 'flavours/glitch/initial_state'; +import { makeGetAccount, getAccountHidden } from 'flavours/glitch/selectors'; + +import Header from '../components/header'; const messages = defineMessages({ cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, - blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, }); const makeMapStateToProps = () => { @@ -46,20 +48,26 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); } } else if (account.getIn(['relationship', 'requested'])) { if (unfollowModal) { - dispatch(openModal('CONFIRM', { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }, })); } else { dispatch(unfollowAccount(account.get('id'))); @@ -70,10 +78,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onInteractionModal (account) { - dispatch(openModal('INTERACTION', { - type: 'follow', - accountId: account.get('id'), - url: account.get('url'), + dispatch(openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'follow', + accountId: account.get('id'), + url: account.get('url'), + }, })); }, @@ -93,10 +104,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(directCompose(account, router)); }, - onDirect (account, router) { - dispatch(directCompose(account, router)); - }, - onReblogToggle (account) { if (account.getIn(['relationship', 'showing_reblogs'])) { dispatch(followAccount(account.get('id'), { reblogs: false })); @@ -138,10 +145,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onBlockDomain (domain) { - dispatch(openModal('CONFIRM', { - message: {domain}
    }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + }, })); }, @@ -150,21 +160,30 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onAddToList (account) { - dispatch(openModal('LIST_ADDER', { - accountId: account.get('id'), + dispatch(openModal({ + modalType: 'LIST_ADDER', + modalProps: { + accountId: account.get('id'), + }, })); }, onChangeLanguages (account) { - dispatch(openModal('SUBSCRIBED_LANGUAGES', { - accountId: account.get('id'), + dispatch(openModal({ + modalType: 'SUBSCRIBED_LANGUAGES', + modalProps: { + accountId: account.get('id'), + }, })); }, onOpenAvatar (account) { - dispatch(openModal('IMAGE', { - src: account.get('avatar'), - alt: account.get('acct'), + dispatch(openModal({ + modalType: 'IMAGE', + modalProps: { + src: account.get('avatar'), + alt: account.get('acct'), + }, })); }, diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.jsx similarity index 92% rename from app/javascript/flavours/glitch/features/account_timeline/index.js rename to app/javascript/flavours/glitch/features/account_timeline/index.jsx index b79082f00a..03c989e969 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.jsx @@ -1,24 +1,34 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; -import { expandAccountFeaturedTimeline, expandAccountTimeline } from 'flavours/glitch/actions/timelines'; -import StatusList from '../../components/status_list'; -import LoadingIndicator from '../../components/loading_indicator'; -import Column from '../ui/components/column'; -import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; -import HeaderContainer from './containers/header_container'; -import ColumnBackButton from 'flavours/glitch/components/column_back_button'; -import { List as ImmutableList } from 'immutable'; -import ImmutablePureComponent from 'react-immutable-pure-component'; + import { FormattedMessage } from 'react-intl'; -import MissingIndicator from 'flavours/glitch/components/missing_indicator'; -import TimelineHint from 'flavours/glitch/components/timeline_hint'; -import LimitedAccountHint from './components/limited_account_hint'; -import { getAccountHidden } from 'flavours/glitch/selectors'; + +import { List as ImmutableList } from 'immutable'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; +import { TimelineHint } from 'flavours/glitch/components/timeline_hint'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; +import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import { getAccountHidden } from 'flavours/glitch/selectors'; + import { fetchFeaturedTags } from '../../actions/featured_tags'; +import { expandAccountFeaturedTimeline, expandAccountTimeline } from '../../actions/timelines'; +import { LoadingIndicator } from '../../components/loading_indicator'; +import StatusList from '../../components/status_list'; +import Column from '../ui/components/column'; + +import LimitedAccountHint from './components/limited_account_hint'; +import HeaderContainer from './containers/header_container'; + + + + + + + const emptyList = ImmutableList(); @@ -62,7 +72,6 @@ RemoteHint.propTypes = { url: PropTypes.string.isRequired, }; -export default @connect(mapStateToProps) class AccountTimeline extends ImmutablePureComponent { static propTypes = { @@ -124,7 +133,7 @@ class AccountTimeline extends ImmutablePureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { const { dispatch } = this.props; if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { @@ -140,15 +149,15 @@ class AccountTimeline extends ImmutablePureComponent { handleHeaderClick = () => { this.column.scrollTop(); - } + }; handleLoadMore = maxId => { this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged })); - } + }; setRef = c => { this.column = c; - } + }; render () { const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props; @@ -161,10 +170,7 @@ class AccountTimeline extends ImmutablePureComponent { ); } else if (!isLoading && !isAccount) { return ( - - - - + ); } @@ -207,3 +213,5 @@ class AccountTimeline extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(AccountTimeline); diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.jsx similarity index 88% rename from app/javascript/flavours/glitch/features/audio/index.js rename to app/javascript/flavours/glitch/features/audio/index.jsx index 014a0a213d..1c41306666 100644 --- a/app/javascript/flavours/glitch/features/audio/index.js +++ b/app/javascript/flavours/glitch/features/audio/index.jsx @@ -1,17 +1,23 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; -import { formatTime } from 'flavours/glitch/features/video'; -import Icon from 'flavours/glitch/components/icon'; + import classNames from 'classnames'; -import { throttle } from 'lodash'; -import { getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video'; -import { debounce } from 'lodash'; -import Visualizer from './visualizer'; -import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; -import Blurhash from 'flavours/glitch/components/blurhash'; + import { is } from 'immutable'; +import { throttle, debounce } from 'lodash'; + +import { Blurhash } from 'flavours/glitch/components/blurhash'; +import { Icon } from 'flavours/glitch/components/icon'; +import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video'; +import { displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; + +import Visualizer from './visualizer'; + + + const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, pause: { id: 'video.pause', defaultMessage: 'Pause' }, @@ -24,12 +30,12 @@ const messages = defineMessages({ const TICK_SIZE = 10; const PADDING = 180; -export default @injectIntl -class Audio extends React.PureComponent { +class Audio extends PureComponent { static propTypes = { src: PropTypes.string.isRequired, alt: PropTypes.string, + lang: PropTypes.string, poster: PropTypes.string, duration: PropTypes.number, width: PropTypes.number, @@ -59,7 +65,7 @@ class Audio extends React.PureComponent { duration: null, paused: true, muted: false, - volume: 0.5, + volume: 1, dragging: false, revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'), }; @@ -75,13 +81,13 @@ class Audio extends React.PureComponent { if (this.player) { this._setDimensions(); } - } + }; _pack() { return { src: this.props.src, - volume: this.audio.volume, - muted: this.audio.muted, + volume: this.state.volume, + muted: this.state.muted, currentTime: this.audio.currentTime, poster: this.props.poster, backgroundColor: this.props.backgroundColor, @@ -96,7 +102,7 @@ class Audio extends React.PureComponent { const width = this.player.offsetWidth; const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9)); - if (width && width != this.state.containerWidth) { + if (width && width !== this.state.containerWidth) { if (this.props.cacheWidth) { this.props.cacheWidth(width); } @@ -107,26 +113,27 @@ class Audio extends React.PureComponent { setSeekRef = c => { this.seek = c; - } + }; setVolumeRef = c => { this.volume = c; - } + }; setAudioRef = c => { this.audio = c; if (this.audio) { - this.setState({ volume: this.audio.volume, muted: this.audio.muted }); + this.audio.volume = 1; + this.audio.muted = false; } - } + }; setCanvasRef = c => { this.canvas = c; this.visualizer.setCanvas(c); - } - + }; + componentDidMount () { window.addEventListener('scroll', this.handleScroll); window.addEventListener('resize', this.handleResize, { passive: true }); @@ -143,7 +150,7 @@ class Audio extends React.PureComponent { } } - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { this.setState({ revealed: nextProps.visible }); } @@ -168,7 +175,7 @@ class Audio extends React.PureComponent { } else { this.setState({ paused: true }, () => this.audio.pause()); } - } + }; handleResize = debounce(() => { if (this.player) { @@ -186,7 +193,7 @@ class Audio extends React.PureComponent { } this._renderCanvas(); - } + }; handlePause = () => { this.setState({ paused: true }); @@ -194,7 +201,7 @@ class Audio extends React.PureComponent { if (this.audioContext) { this.audioContext.suspend(); } - } + }; handleProgress = () => { const lastTimeRange = this.audio.buffered.length - 1; @@ -202,15 +209,17 @@ class Audio extends React.PureComponent { if (lastTimeRange > -1) { this.setState({ buffer: Math.ceil(this.audio.buffered.end(lastTimeRange) / this.audio.duration * 100) }); } - } + }; toggleMute = () => { const muted = !this.state.muted; this.setState({ muted }, () => { - this.audio.muted = muted; + if (this.gainNode) { + this.gainNode.gain.value = muted ? 0 : this.state.volume; + } }); - } + }; toggleReveal = () => { if (this.props.onToggleVisibility) { @@ -218,7 +227,7 @@ class Audio extends React.PureComponent { } else { this.setState({ revealed: !this.state.revealed }); } - } + }; handleVolumeMouseDown = e => { document.addEventListener('mousemove', this.handleMouseVolSlide, true); @@ -230,14 +239,14 @@ class Audio extends React.PureComponent { e.preventDefault(); e.stopPropagation(); - } + }; handleVolumeMouseUp = () => { document.removeEventListener('mousemove', this.handleMouseVolSlide, true); document.removeEventListener('mouseup', this.handleVolumeMouseUp, true); document.removeEventListener('touchmove', this.handleMouseVolSlide, true); document.removeEventListener('touchend', this.handleVolumeMouseUp, true); - } + }; handleMouseDown = e => { document.addEventListener('mousemove', this.handleMouseMove, true); @@ -251,7 +260,7 @@ class Audio extends React.PureComponent { e.preventDefault(); e.stopPropagation(); - } + }; handleMouseUp = () => { document.removeEventListener('mousemove', this.handleMouseMove, true); @@ -261,7 +270,7 @@ class Audio extends React.PureComponent { this.setState({ dragging: false }); this.audio.play(); - } + }; handleMouseMove = throttle(e => { const { x } = getPointerPosition(this.seek, e); @@ -279,14 +288,16 @@ class Audio extends React.PureComponent { currentTime: this.audio.currentTime, duration: this.audio.duration, }); - } + }; handleMouseVolSlide = throttle(e => { const { x } = getPointerPosition(this.volume, e); if(!isNaN(x)) { this.setState({ volume: x }, () => { - this.audio.volume = x; + if (this.gainNode) { + this.gainNode.gain.value = this.state.muted ? 0 : x; + } }); } }, 15); @@ -312,41 +323,38 @@ class Audio extends React.PureComponent { handleMouseEnter = () => { this.setState({ hovered: true }); - } + }; handleMouseLeave = () => { this.setState({ hovered: false }); - } + }; handleLoadedData = () => { - const { autoPlay, currentTime, volume, muted } = this.props; + const { autoPlay, currentTime } = this.props; if (currentTime) { this.audio.currentTime = currentTime; } - if (volume !== undefined) { - this.audio.volume = volume; - } - - if (muted !== undefined) { - this.audio.muted = muted; - } - if (autoPlay) { this.togglePlay(); } - } + }; _initAudioContext () { const AudioContext = window.AudioContext || window.webkitAudioContext; const context = new AudioContext(); const source = context.createMediaElementSource(this.audio); + const gainNode = context.createGain(); + + gainNode.gain.value = this.state.muted ? 0 : this.state.volume; this.visualizer.setAudioContext(context, source); - source.connect(context.destination); + source.connect(gainNode); + gainNode.connect(context.destination); this.audioContext = context; + this.gainNode = gainNode; } handleDownload = () => { @@ -365,7 +373,7 @@ class Audio extends React.PureComponent { }).catch(err => { console.error(err); }); - } + }; _renderCanvas () { requestAnimationFrame(() => { @@ -390,7 +398,7 @@ class Audio extends React.PureComponent { } _getRadius () { - return parseInt(((this.state.height || this.props.height) - (PADDING * this._getScaleCoefficient()) * 2) / 2); + return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient()); } _getScaleCoefficient () { @@ -402,7 +410,7 @@ class Audio extends React.PureComponent { } _getCY() { - return Math.floor(this._getRadius() + (PADDING * this._getScaleCoefficient())); + return Math.floor((this.state.height || this.props.height) / 2); } _getAccentColor () { @@ -436,7 +444,7 @@ class Audio extends React.PureComponent { e.stopPropagation(); this.togglePlay(); } - } + }; handleKeyDown = e => { switch(e.key) { @@ -461,10 +469,10 @@ class Audio extends React.PureComponent { this.seekBy(10); break; } - } + }; render () { - const { src, intl, alt, editable, autoPlay, sensitive, blurhash } = this.props; + const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props; const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); @@ -476,7 +484,7 @@ class Audio extends React.PureComponent { } return ( -
    +
    @@ -520,9 +529,16 @@ class Audio extends React.PureComponent { {(revealed || editable) && }
    @@ -531,7 +547,7 @@ class Audio extends React.PureComponent { @@ -548,7 +564,7 @@ class Audio extends React.PureComponent {
    @@ -573,3 +589,5 @@ class Audio extends React.PureComponent { } } + +export default injectIntl(Audio); diff --git a/app/javascript/flavours/glitch/features/blocks/index.js b/app/javascript/flavours/glitch/features/blocks/index.jsx similarity index 92% rename from app/javascript/flavours/glitch/features/blocks/index.js rename to app/javascript/flavours/glitch/features/blocks/index.jsx index 4461bd14d8..aa5479b208 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.js +++ b/app/javascript/flavours/glitch/features/blocks/index.jsx @@ -1,16 +1,20 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { debounce } from 'lodash'; import PropTypes from 'prop-types'; -import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; -import ScrollableList from '../../components/scrollable_list'; -import Column from 'flavours/glitch/features/ui/components/column'; -import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; -import AccountContainer from 'flavours/glitch/containers/account_container'; -import { fetchBlocks, expandBlocks } from 'flavours/glitch/actions/blocks'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { connect } from 'react-redux'; + +import { debounce } from 'lodash'; + +import { fetchBlocks, expandBlocks } from 'flavours/glitch/actions/blocks'; +import ColumnBackButtonSlim from 'flavours/glitch/components/column_back_button_slim'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import AccountContainer from 'flavours/glitch/containers/account_container'; +import Column from 'flavours/glitch/features/ui/components/column'; + +import ScrollableList from '../../components/scrollable_list'; const messages = defineMessages({ heading: { id: 'column.blocks', defaultMessage: 'Blocked users' }, @@ -22,8 +26,6 @@ const mapStateToProps = state => ({ isLoading: state.getIn(['user_lists', 'blocks', 'isLoading'], true), }); -export default @connect(mapStateToProps) -@injectIntl class Blocks extends ImmutablePureComponent { static propTypes = { @@ -36,7 +38,7 @@ class Blocks extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBlocks()); } @@ -77,3 +79,5 @@ class Blocks extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(injectIntl(Blocks)); diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx similarity index 92% rename from app/javascript/flavours/glitch/features/bookmarked_statuses/index.js rename to app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx index 8978ac5fc1..c674c82547 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx @@ -1,29 +1,32 @@ -import { debounce } from 'lodash'; import PropTypes from 'prop-types'; -import React from 'react'; -import { Helmet } from 'react-helmet'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; + +import { debounce } from 'lodash'; + import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import ColumnHeader from 'flavours/glitch/components/column_header'; import StatusList from 'flavours/glitch/components/status_list'; import Column from 'flavours/glitch/features/ui/components/column'; +import { getStatusList } from 'flavours/glitch/selectors'; const messages = defineMessages({ heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' }, }); const mapStateToProps = state => ({ - statusIds: state.getIn(['status_lists', 'bookmarks', 'items']), + statusIds: getStatusList(state, 'bookmarks'), isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true), hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), }); -export default @connect(mapStateToProps) -@injectIntl class Bookmarks extends ImmutablePureComponent { static propTypes = { @@ -36,7 +39,7 @@ class Bookmarks extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - componentWillMount () { + UNSAFE_componentWillMount () { this.props.dispatch(fetchBookmarkedStatuses()); } @@ -48,24 +51,24 @@ class Bookmarks extends ImmutablePureComponent { } else { dispatch(addColumn('BOOKMARKS', {})); } - } + }; handleMove = (dir) => { const { columnId, dispatch } = this.props; dispatch(moveColumn(columnId, dir)); - } + }; handleHeaderClick = () => { this.column.scrollTop(); - } + }; setRef = c => { this.column = c; - } + }; handleLoadMore = debounce(() => { this.props.dispatch(expandBookmarkedStatuses()); - }, 300, { leading: true }) + }, 300, { leading: true }); render () { const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; @@ -106,3 +109,5 @@ class Bookmarks extends ImmutablePureComponent { } } + +export default connect(mapStateToProps)(injectIntl(Bookmarks)); diff --git a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.js b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx similarity index 97% rename from app/javascript/flavours/glitch/features/closed_registrations_modal/index.js rename to app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx index cb91636cb8..b556da3911 100644 --- a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.js +++ b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx @@ -1,15 +1,15 @@ -import React from 'react'; -import { connect } from 'react-redux'; import { FormattedMessage } from 'react-intl'; + import ImmutablePureComponent from 'react-immutable-pure-component'; -import { domain } from 'flavours/glitch/initial_state'; +import { connect } from 'react-redux'; + import { fetchServer } from 'flavours/glitch/actions/server'; +import { domain } from 'flavours/glitch/initial_state'; const mapStateToProps = state => ({ message: state.getIn(['server', 'server', 'registrations', 'message']), }); -export default @connect(mapStateToProps) class ClosedRegistrationsModal extends ImmutablePureComponent { componentDidMount () { @@ -72,4 +72,6 @@ class ClosedRegistrationsModal extends ImmutablePureComponent { ); } -}; +} + +export default connect(mapStateToProps)(ClosedRegistrationsModal); diff --git a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx similarity index 91% rename from app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js rename to app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx index 69a4699acd..1e93125d59 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js +++ b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx @@ -1,7 +1,10 @@ -import React from 'react'; import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + import SettingText from 'flavours/glitch/components/setting_text'; import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle'; @@ -10,8 +13,7 @@ const messages = defineMessages({ settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -export default @injectIntl -class ColumnSettings extends React.PureComponent { +class ColumnSettings extends PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, @@ -39,3 +41,5 @@ class ColumnSettings extends React.PureComponent { } } + +export default injectIntl(ColumnSettings); diff --git a/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js index b892f08ade..dbfc4594e1 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js @@ -1,8 +1,10 @@ import { connect } from 'react-redux'; -import ColumnSettings from '../components/column_settings'; + import { changeColumnParams } from 'flavours/glitch/actions/columns'; import { changeSetting } from 'flavours/glitch/actions/settings'; +import ColumnSettings from '../components/column_settings'; + const mapStateToProps = (state, { columnId }) => { const uuid = columnId; const columns = state.getIn(['settings', 'columns']); @@ -12,7 +14,7 @@ const mapStateToProps = (state, { columnId }) => { settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']), }; }; - + const mapDispatchToProps = (dispatch, { columnId }) => { return { onChange (key, checked) { diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.jsx similarity index 91% rename from app/javascript/flavours/glitch/features/community_timeline/index.js rename to app/javascript/flavours/glitch/features/community_timeline/index.jsx index 67bf548755..ca11adb464 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.jsx @@ -1,17 +1,22 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { connect } from 'react-redux'; + +import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; +import { connectCommunityStream } from 'flavours/glitch/actions/streaming'; +import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines'; -import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; -import ColumnSettingsContainer from './containers/column_settings_container'; -import { connectCommunityStream } from 'flavours/glitch/actions/streaming'; -import { Helmet } from 'react-helmet'; -import { domain } from 'flavours/glitch/initial_state'; import DismissableBanner from 'flavours/glitch/components/dismissable_banner'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import { domain } from 'flavours/glitch/initial_state'; + +import ColumnSettingsContainer from './containers/column_settings_container'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, @@ -32,9 +37,7 @@ const mapStateToProps = (state, { columnId }) => { }; }; -export default @connect(mapStateToProps) -@injectIntl -class CommunityTimeline extends React.PureComponent { +class CommunityTimeline extends PureComponent { static defaultProps = { onlyMedia: false, @@ -63,16 +66,16 @@ class CommunityTimeline extends React.PureComponent { } else { dispatch(addColumn('COMMUNITY', { other: { onlyMedia } })); } - } + }; handleMove = (dir) => { const { columnId, dispatch } = this.props; dispatch(moveColumn(columnId, dir)); - } + }; handleHeaderClick = () => { this.column.scrollTop(); - } + }; componentDidMount () { const { dispatch, onlyMedia } = this.props; @@ -112,13 +115,13 @@ class CommunityTimeline extends React.PureComponent { setRef = c => { this.column = c; - } + }; handleLoadMore = maxId => { const { dispatch, onlyMedia } = this.props; dispatch(expandCommunityTimeline({ maxId, onlyMedia })); - } + }; render () { const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props; @@ -139,11 +142,8 @@ class CommunityTimeline extends React.PureComponent { - - - - } trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} timelineId={`community${onlyMedia ? ':media' : ''}`} @@ -162,3 +162,5 @@ class CommunityTimeline extends React.PureComponent { } } + +export default connect(mapStateToProps)(injectIntl(CommunityTimeline)); diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.js b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx similarity index 92% rename from app/javascript/flavours/glitch/features/compose/components/action_bar.js rename to app/javascript/flavours/glitch/features/compose/components/action_bar.jsx index 838ef09ea9..f155979ef9 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -1,10 +1,14 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; + const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, @@ -14,15 +18,14 @@ const messages = defineMessages({ lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, }); -export default @injectIntl -class ActionBar extends React.PureComponent { +class ActionBar extends PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, @@ -32,7 +35,7 @@ class ActionBar extends React.PureComponent { handleLogout = () => { this.props.onLogout(); - } + }; render () { const { intl } = this.props; @@ -59,10 +62,12 @@ class ActionBar extends React.PureComponent { return (
    - +
    ); } } + +export default injectIntl(ActionBar); diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx similarity index 79% rename from app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js rename to app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx index fb9bb50358..5f00da52c4 100644 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx @@ -1,9 +1,9 @@ -import React from 'react'; -import Avatar from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { Avatar } from 'flavours/glitch/components/avatar'; +import { DisplayName } from 'flavours/glitch/components/display_name'; + export default class AutosuggestAccount extends ImmutablePureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/compose/components/character_counter.js b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx similarity index 83% rename from app/javascript/flavours/glitch/features/compose/components/character_counter.js rename to app/javascript/flavours/glitch/features/compose/components/character_counter.jsx index 0ecfc9141d..42452b30f6 100644 --- a/app/javascript/flavours/glitch/features/compose/components/character_counter.js +++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx @@ -1,8 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { length } from 'stringz'; -export default class CharacterCounter extends React.PureComponent { +export default class CharacterCounter extends PureComponent { static propTypes = { text: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx similarity index 91% rename from app/javascript/flavours/glitch/features/compose/components/compose_form.js rename to app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index 0462c7c4b7..53e1bf79ae 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -1,34 +1,42 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import ReplyIndicatorContainer from '../containers/reply_indicator_container'; -import AutosuggestTextarea from '../../../components/autosuggest_textarea'; -import AutosuggestInput from '../../../components/autosuggest_input'; + import { defineMessages, injectIntl } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +import { length } from 'stringz'; + +import { maxChars } from 'flavours/glitch/initial_state'; +import { isMobile } from 'flavours/glitch/is_mobile'; + +import AutosuggestInput from '../../../components/autosuggest_input'; +import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; +import OptionsContainer from '../containers/options_container'; import PollFormContainer from '../containers/poll_form_container'; +import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import UploadFormContainer from '../containers/upload_form_container'; import WarningContainer from '../containers/warning_container'; -import { isMobile } from 'flavours/glitch/is_mobile'; -import ImmutablePureComponent from 'react-immutable-pure-component'; import { countableText } from '../util/counter'; -import OptionsContainer from '../containers/options_container'; + +import CharacterCounter from './character_counter'; import Publisher from './publisher'; import TextareaIcons from './textarea_icons'; -import { maxChars } from 'flavours/glitch/initial_state'; -import CharacterCounter from './character_counter'; -import { length } from 'stringz'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, - missingDescriptionMessage: { id: 'confirmations.missing_media_description.message', - defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' }, - missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm', - defaultMessage: 'Send anyway' }, + missingDescriptionMessage: { + id: 'confirmations.missing_media_description.message', + defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.', + }, + missingDescriptionConfirm: { + id: 'confirmations.missing_media_description.confirm', + defaultMessage: 'Send anyway', + }, spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, }); -export default @injectIntl class ComposeForm extends ImmutablePureComponent { static contextTypes = { @@ -61,6 +69,7 @@ class ComposeForm extends ImmutablePureComponent { anyMedia: PropTypes.bool, isInReply: PropTypes.bool, singleColumn: PropTypes.bool, + lang: PropTypes.string, advancedOptions: ImmutablePropTypes.map, layout: PropTypes.string, @@ -72,7 +81,6 @@ class ComposeForm extends ImmutablePureComponent { preselectOnReply: PropTypes.bool, onChangeSpoilerness: PropTypes.func, onChangeVisibility: PropTypes.func, - onPaste: PropTypes.func, onMediaDescriptionConfirm: PropTypes.func, }; @@ -82,22 +90,22 @@ class ComposeForm extends ImmutablePureComponent { handleChange = (e) => { this.props.onChange(e.target.value); - } + }; getFulltextForCharacterCounting = () => { return [ this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text), - this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' 👁️' : '' + this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' 👁️' : '', ].join(''); - } + }; canSubmit = () => { const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props; const fulltext = this.getFulltextForCharacterCounting(); return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (!fulltext.trim().length && !anyMedia)); - } + }; handleSubmit = (overriddenVisibility = null) => { const { @@ -128,7 +136,7 @@ class ComposeForm extends ImmutablePureComponent { } onSubmit(this.context.router ? this.context.router.history : null); } - } + }; // Changes the text value of the spoiler. handleChangeSpoiler = ({ target: { value } }) => { @@ -136,7 +144,7 @@ class ComposeForm extends ImmutablePureComponent { if (onChangeSpoilerText) { onChangeSpoilerText(value); } - } + }; setRef = c => { this.composeForm = c; @@ -149,7 +157,7 @@ class ComposeForm extends ImmutablePureComponent { if (onPickEmoji) { onPickEmoji(selectionStart, data); } - } + }; // Handles the secondary submit button. handleSecondarySubmit = () => { @@ -157,40 +165,40 @@ class ComposeForm extends ImmutablePureComponent { sideArm, } = this.props; this.handleSubmit(sideArm === 'none' ? null : sideArm); - } + }; // Selects a suggestion from the autofill. - onSuggestionSelected = (tokenStart, token, value) => { + handleSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['text']); - } + }; - onSpoilerSuggestionSelected = (tokenStart, token, value) => { + handleSpoilerSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); - } + }; handleKeyDown = (e) => { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { this.handleSubmit(); } - if (e.keyCode == 13 && e.altKey) { + if (e.keyCode === 13 && e.altKey) { this.handleSecondarySubmit(); } - } + }; // Sets a reference to the textarea. setAutosuggestTextarea = (textareaComponent) => { if (textareaComponent) { this.textarea = textareaComponent.textarea; } - } + }; // Sets a reference to the CW field. handleRefSpoilerText = (spoilerComponent) => { if (spoilerComponent) { this.spoilerText = spoilerComponent.input; } - } + }; handleFocus = () => { if (this.composeForm && !this.props.singleColumn) { @@ -199,7 +207,7 @@ class ComposeForm extends ImmutablePureComponent { this.composeForm.scrollIntoView(); } } - } + }; componentDidMount () { this._updateFocusAndSelection({ }); @@ -216,7 +224,7 @@ class ComposeForm extends ImmutablePureComponent { // - Replying to more than one user, selects any usernames past // the first; this provides a convenient shortcut to drop // everyone else from the conversation. - _updateFocusAndSelection = (prevProps) => { + _updateFocusAndSelection = (prevProps) => { const { textarea, spoilerText, @@ -270,16 +278,14 @@ class ComposeForm extends ImmutablePureComponent { } } } - } + }; render () { const { handleEmojiPick, handleSecondarySubmit, - handleSelect, handleSubmit, - handleRefTextarea, } = this; const { advancedOptions, @@ -287,7 +293,6 @@ class ComposeForm extends ImmutablePureComponent { isSubmitting, layout, onChangeSpoilerness, - onChangeVisibility, onClearSuggestions, onFetchSuggestions, onPaste, @@ -318,14 +323,16 @@ class ComposeForm extends ImmutablePureComponent { onKeyDown={this.handleKeyDown} disabled={!spoiler} ref={this.handleRefSpoilerText} - suggestions={this.props.suggestions} + suggestions={suggestions} onSuggestionsFetchRequested={onFetchSuggestions} onSuggestionsClearRequested={onClearSuggestions} - onSuggestionSelected={this.onSpoilerSuggestionSelected} + onSuggestionSelected={this.handleSpoilerSuggestionSelected} searchTokens={[':']} id='glitch.composer.spoiler.input' className='spoiler-input__input' + lang={this.props.lang} autoFocus={false} + spellCheck />
    @@ -336,13 +343,14 @@ class ComposeForm extends ImmutablePureComponent { value={this.props.text} onChange={this.handleChange} onKeyDown={this.handleKeyDown} - suggestions={this.props.suggestions} + suggestions={suggestions} onFocus={this.handleFocus} onSuggestionsFetchRequested={onFetchSuggestions} onSuggestionsClearRequested={onClearSuggestions} - onSuggestionSelected={this.onSuggestionSelected} + onSuggestionSelected={this.handleSuggestionSelected} onPaste={onPaste} autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} + lang={this.props.lang} > @@ -381,3 +389,5 @@ class ComposeForm extends ImmutablePureComponent { } } + +export default injectIntl(ComposeForm); diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx similarity index 92% rename from app/javascript/flavours/glitch/features/compose/components/dropdown.js rename to app/javascript/flavours/glitch/features/compose/components/dropdown.jsx index d98b311d9a..a50413ebe6 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx @@ -1,18 +1,18 @@ // Package imports. -import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; +import { PureComponent } from 'react'; + +import classNames from 'classnames'; + import Overlay from 'react-overlays/Overlay'; // Components. -import IconButton from 'flavours/glitch/components/icon_button'; +import { IconButton } from 'flavours/glitch/components/icon_button'; + import DropdownMenu from './dropdown_menu'; -// Utils. -import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; - // The component. -export default class ComposerOptionsDropdown extends React.PureComponent { +export default class ComposerOptionsDropdown extends PureComponent { static propTypes = { isUserTouching: PropTypes.func, @@ -50,7 +50,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { const { open } = this.state; if (this.props.isUserTouching && this.props.isUserTouching()) { - if (this.state.open) { + if (open) { this.props.onModalClose(); } else { const modal = this.handleMakeModal(); @@ -59,12 +59,12 @@ export default class ComposerOptionsDropdown extends React.PureComponent { } } } else { - if (this.state.open && this.activeElement) { + if (open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' }); + this.setState({ open: !open, openedViaKeyboard: type !== 'click' }); } - } + }; handleKeyDown = (e) => { switch (e.key) { @@ -72,13 +72,13 @@ export default class ComposerOptionsDropdown extends React.PureComponent { this.handleClose(); break; } - } + }; handleMouseDown = () => { if (!this.state.open) { this.activeElement = document.activeElement; } - } + }; handleButtonKeyDown = (e) => { switch(e.key) { @@ -87,7 +87,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { this.handleMouseDown(); break; } - } + }; handleKeyPress = (e) => { switch(e.key) { @@ -98,14 +98,14 @@ export default class ComposerOptionsDropdown extends React.PureComponent { e.preventDefault(); break; } - } + }; handleClose = () => { if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } this.setState({ open: false }); - } + }; handleItemClick = (e) => { const { @@ -151,22 +151,22 @@ export default class ComposerOptionsDropdown extends React.PureComponent { ...rest, active: value && name === value, name, - }) + }), ), }; - } + }; setTargetRef = c => { this.target = c; - } + }; findTarget = () => { return this.target; - } + }; handleOverlayEnter = (state) => { this.setState({ placement: state.placement }); - } + }; // Rendering. render () { diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx similarity index 86% rename from app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js rename to app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx index c4895dfd0d..a78e6914cf 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx @@ -1,18 +1,18 @@ // Package imports. import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { PureComponent } from 'react'; + import classNames from 'classnames'; -// Components. -import Icon from 'flavours/glitch/components/icon'; +import { supportsPassiveEvents } from 'detect-passive-events'; -// Utils. -import { withPassive } from 'flavours/glitch/utils/dom_helpers'; -import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; +// Components. +import { Icon } from 'flavours/glitch/components/icon'; + +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; // The component. -export default class ComposerOptionsDropdownContent extends React.PureComponent { +export default class ComposerOptionsDropdownContent extends PureComponent { static propTypes = { items: PropTypes.arrayOf(PropTypes.shape({ @@ -43,18 +43,19 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent handleDocumentClick = (e) => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); + e.stopPropagation(); } - } + }; // Stores our node in `this.node`. setRef = (node) => { this.node = node; - } + }; // On mounting, we add our listeners. componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, withPassive); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem) { this.focusedItem.focus({ preventScroll: true }); } else { @@ -64,8 +65,8 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // On unmounting, we remove our listeners. componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, withPassive); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } handleClick = (e) => { @@ -78,13 +79,14 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent items, } = this.props; - const { name } = this.props.items[i]; + const { name } = items[i]; + e.preventDefault(); // Prevents change in focus on click if (closeOnChange) { onClose(); } onChange(name); - } + }; // Handle changes differently whether the dropdown is a list of options or actions handleChange = (name) => { @@ -93,7 +95,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } else { this.setState({ value: name }); } - } + }; handleKeyDown = (e) => { const index = Number(e.currentTarget.getAttribute('data-index')); @@ -131,15 +133,15 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent if (element) { element.focus(); - this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name); + this.handleChange(items[Number(element.getAttribute('data-index'))].name); e.preventDefault(); e.stopPropagation(); } - } + }; setFocusRef = c => { this.focusedItem = c; - } + }; renderItem = (item, i) => { const { name, icon, meta, text } = item; @@ -152,14 +154,14 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent if (!contents) { contents = ( - + <> {icon && }
    {text} {meta}
    -
    + ); } @@ -169,7 +171,8 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent onClick={this.handleClick} onKeyDown={this.handleKeyDown} role='option' - tabIndex='0' + aria-selected={active} + tabIndex={0} key={name} data-index={i} ref={active ? this.setFocusRef : null} @@ -177,14 +180,12 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent {contents}
    ); - } + }; // Rendering. render () { const { items, - onChange, - onClose, style, } = this.props; diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx similarity index 94% rename from app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js rename to app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index 38c7355514..c2c8030615 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -1,15 +1,21 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; -import Overlay from 'react-overlays/Overlay'; + import classNames from 'classnames'; + import ImmutablePropTypes from 'react-immutable-proptypes'; + import { supportsPassiveEvents } from 'detect-passive-events'; -import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; +import Overlay from 'react-overlays/Overlay'; + import { useSystemEmojiFont } from 'flavours/glitch/initial_state'; import { assetHost } from 'flavours/glitch/utils/config'; +import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; +import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; + const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, @@ -28,7 +34,7 @@ const messages = defineMessages({ let EmojiPicker, Emoji; // load asynchronously -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; +const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`; @@ -48,7 +54,7 @@ const notFoundFn = () => (
    ); -class ModifierPickerMenu extends React.PureComponent { +class ModifierPickerMenu extends PureComponent { static propTypes = { active: PropTypes.bool, @@ -58,9 +64,9 @@ class ModifierPickerMenu extends React.PureComponent { handleClick = e => { this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); - } + }; - componentWillReceiveProps (nextProps) { + UNSAFE_componentWillReceiveProps (nextProps) { if (nextProps.active) { this.attachListeners(); } else { @@ -76,21 +82,21 @@ class ModifierPickerMenu extends React.PureComponent { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); } - } + }; attachListeners () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); } removeListeners () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } setRef = c => { this.node = c; - } + }; render () { const { active } = this.props; @@ -109,7 +115,7 @@ class ModifierPickerMenu extends React.PureComponent { } -class ModifierPicker extends React.PureComponent { +class ModifierPicker extends PureComponent { static propTypes = { active: PropTypes.bool, @@ -125,12 +131,12 @@ class ModifierPicker extends React.PureComponent { } else { this.props.onOpen(); } - } + }; handleSelect = modifier => { this.props.onChange(modifier); this.props.onClose(); - } + }; render () { const { active, modifier } = this.props; @@ -145,8 +151,7 @@ class ModifierPicker extends React.PureComponent { } -@injectIntl -class EmojiPickerMenu extends React.PureComponent { +class EmojiPickerMenuImpl extends PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, @@ -175,10 +180,10 @@ class EmojiPickerMenu extends React.PureComponent { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); } - } + }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('click', this.handleDocumentClick, { capture: true }); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need @@ -193,13 +198,13 @@ class EmojiPickerMenu extends React.PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('click', this.handleDocumentClick, { capture: true }); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } setRef = c => { this.node = c; - } + }; getI18n = () => { const { intl } = this.props; @@ -220,7 +225,7 @@ class EmojiPickerMenu extends React.PureComponent { custom: intl.formatMessage(messages.custom), }, }; - } + }; handleClick = (emoji, event) => { if (!emoji.native) { @@ -230,19 +235,19 @@ class EmojiPickerMenu extends React.PureComponent { this.props.onClose(); } this.props.onPick(emoji); - } + }; handleModifierOpen = () => { this.setState({ modifierOpen: true }); - } + }; handleModifierClose = () => { this.setState({ modifierOpen: false }); - } + }; handleModifierChange = modifier => { this.props.onSkinTone(modifier); - } + }; render () { const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; @@ -307,8 +312,9 @@ class EmojiPickerMenu extends React.PureComponent { } -export default @injectIntl -class EmojiPickerDropdown extends React.PureComponent { +const EmojiPickerMenu = injectIntl(EmojiPickerMenuImpl); + +class EmojiPickerDropdown extends PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, @@ -327,7 +333,7 @@ class EmojiPickerDropdown extends React.PureComponent { setRef = (c) => { this.dropdown = c; - } + }; onShowDropdown = () => { this.setState({ active: true }); @@ -344,11 +350,11 @@ class EmojiPickerDropdown extends React.PureComponent { this.setState({ loading: false, active: false }); }); } - } + }; onHideDropdown = () => { this.setState({ active: false }); - } + }; onToggle = (e) => { if (!this.state.loading && (!e.key || e.key === 'Enter')) { @@ -358,21 +364,21 @@ class EmojiPickerDropdown extends React.PureComponent { this.onShowDropdown(e); } } - } + }; handleKeyDown = e => { if (e.key === 'Escape') { this.onHideDropdown(); } - } + }; setTargetRef = c => { this.target = c; - } + }; findTarget = () => { return this.target; - } + }; render () { const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; @@ -385,7 +391,7 @@ class EmojiPickerDropdown extends React.PureComponent { {button || 🙂}
    @@ -411,3 +417,5 @@ class EmojiPickerDropdown extends React.PureComponent { } } + +export default injectIntl(EmojiPickerDropdown); diff --git a/app/javascript/flavours/glitch/features/compose/components/header.js b/app/javascript/flavours/glitch/features/compose/components/header.jsx similarity index 92% rename from app/javascript/flavours/glitch/features/compose/components/header.js rename to app/javascript/flavours/glitch/features/compose/components/header.jsx index 7ecb573aba..ac6d4dce85 100644 --- a/app/javascript/flavours/glitch/features/compose/components/header.js +++ b/app/javascript/flavours/glitch/features/compose/components/header.jsx @@ -1,19 +1,16 @@ -// Package imports. import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; + import { injectIntl, defineMessages } from 'react-intl'; + import { Link } from 'react-router-dom'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; +import { Icon } from 'flavours/glitch/components/icon'; import { signOutLink } from 'flavours/glitch/utils/backend_links'; +import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; -// Messages. const messages = defineMessages({ community: { defaultMessage: 'Local timeline', @@ -45,8 +42,8 @@ const messages = defineMessages({ }, }); -export default @injectIntl class Header extends ImmutablePureComponent { + static propTypes = { columns: ImmutablePropTypes.list, unreadNotifications: PropTypes.number, @@ -63,7 +60,7 @@ class Header extends ImmutablePureComponent { this.props.onLogout(); return false; - } + }; render () { const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props; @@ -71,13 +68,13 @@ class Header extends ImmutablePureComponent { // Only renders the component if the column isn't being shown. const renderForColumn = conditionalRender.bind(null, columnId => !columns || !columns.some( - column => column.get('id') === columnId - ) + column => column.get('id') === columnId, + ), ); // The result. return ( -