From 9d44f828176137c597199d301ba22878174be993 Mon Sep 17 00:00:00 2001 From: InterSocial <110579033+glitchscorpion@users.noreply.github.com> Date: Wed, 14 Jun 2023 22:41:32 -0400 Subject: [PATCH] Quotes --- Aptfile | 1 - CHANGELOG.md | 65 - CONTRIBUTING.md | 16 +- Dockerfile | 17 +- Gemfile | 120 +- Gemfile.lock | 240 +- README.md | 15 +- 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 | 10 + .../admin/domain_blocks_controller.rb | 46 +- .../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 | 18 +- app/controllers/api/base_controller.rb | 9 +- .../api/v1/accounts/credentials_controller.rb | 12 +- .../accounts/follower_accounts_controller.rb | 1 - .../accounts/following_accounts_controller.rb | 1 - .../api/v1/accounts/lookup_controller.rb | 1 - .../api/v1/accounts/statuses_controller.rb | 1 - app/controllers/api/v1/accounts_controller.rb | 3 +- .../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 +- .../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/conversations_controller.rb | 15 +- .../api/v1/custom_emojis_controller.rb | 5 +- .../api/v1/directories_controller.rb | 1 - .../api/v1/emails/confirmations_controller.rb | 11 +- .../api/v1/featured_tags_controller.rb | 6 +- app/controllers/api/v1/filters_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 | 4 +- .../api/v1/instances_controller.rb | 11 +- app/controllers/api/v1/lists_controller.rb | 2 +- app/controllers/api/v1/media_controller.rb | 11 +- app/controllers/api/v1/polls_controller.rb | 1 - .../api/v1/push/subscriptions_controller.rb | 8 +- .../favourited_by_accounts_controller.rb | 1 - .../api/v1/statuses/histories_controller.rb | 1 - .../api/v1/statuses/reactions_controller.rb | 12 +- .../reblogged_by_accounts_controller.rb | 1 - .../api/v1/statuses/reblogs_controller.rb | 6 +- app/controllers/api/v1/statuses_controller.rb | 7 +- .../api/v1/streaming_controller.rb | 2 +- app/controllers/api/v1/tags_controller.rb | 1 - .../api/v1/timelines/public_controller.rb | 1 - .../api/v1/timelines/tag_controller.rb | 1 - .../api/v1/trends/links_controller.rb | 3 - .../api/v1/trends/statuses_controller.rb | 3 - .../api/v1/trends/tags_controller.rb | 1 - .../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/application_controller.rb | 31 +- .../auth/confirmations_controller.rb | 20 +- .../auth/registrations_controller.rb | 14 +- app/controllers/auth/setup_controller.rb | 21 +- .../authorize_interactions_controller.rb | 2 +- app/controllers/backups_controller.rb | 10 +- .../concerns/account_controller_concern.rb | 3 +- app/controllers/concerns/cache_concern.rb | 32 +- app/controllers/concerns/captcha_concern.rb | 5 - .../concerns/signature_verification.rb | 9 +- .../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 | 5 - 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 - app/controllers/manifests_controller.rb | 7 +- app/controllers/media_controller.rb | 3 +- 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 | 5 - .../settings/applications_controller.rb | 4 +- 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 +- .../preferences/notifications_controller.rb | 2 +- .../settings/preferences/other_controller.rb | 2 +- .../settings/preferences_controller.rb | 66 + .../otp_authentication_controller.rb | 9 + .../webauthn_credentials_controller.rb | 3 +- .../statuses_cleanup_controller.rb | 5 - app/controllers/statuses_controller.rb | 12 +- app/controllers/tags_controller.rb | 4 +- .../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/application_helper.rb | 40 +- app/helpers/branding_helper.rb | 4 +- app/helpers/context_helper.rb | 1 + app/helpers/formatting_helper.rb | 12 +- app/helpers/home_helper.rb | 2 +- app/helpers/instance_helper.rb | 18 +- app/helpers/jsonld_helper.rb | 8 +- app/helpers/languages_helper.rb | 3 + app/helpers/settings_helper.rb | 4 + app/helpers/statuses_helper.rb | 88 +- app/javascript/core/admin.js | 1 - app/javascript/core/mailer.js | 2 +- app/javascript/core/public.js | 4 +- app/javascript/core/settings.js | 2 +- app/javascript/core/theme.yml | 1 - .../core/two_factor_authentication.js | 4 +- .../flavours/glitch/actions/accounts.js | 33 +- .../flavours/glitch/actions/announcements.js | 1 - app/javascript/flavours/glitch/actions/app.js | 6 + .../flavours/glitch/actions/blocks.js | 3 +- .../flavours/glitch/actions/bookmarks.js | 1 - .../flavours/glitch/actions/boosts.js | 5 +- .../flavours/glitch/actions/compose.js | 108 +- .../flavours/glitch/actions/conversations.js | 1 - .../flavours/glitch/actions/directory.js | 3 +- .../flavours/glitch/actions/favourites.js | 1 - .../flavours/glitch/actions/filters.js | 10 +- .../flavours/glitch/actions/history.js | 1 - .../glitch/actions/importer/normalizer.js | 72 +- .../flavours/glitch/actions/interactions.js | 1 - .../flavours/glitch/actions/lists.js | 7 +- .../flavours/glitch/actions/local_settings.js | 10 +- .../flavours/glitch/actions/markers.js | 10 +- .../flavours/glitch/actions/modal.js | 18 + .../flavours/glitch/actions/mutes.js | 6 +- .../flavours/glitch/actions/notifications.js | 21 +- .../flavours/glitch/actions/onboarding.js | 4 +- .../glitch/actions/picture_in_picture.js | 3 +- .../flavours/glitch/actions/pin_statuses.js | 6 +- .../flavours/glitch/actions/polls.js | 1 - .../actions/push_notifications/index.js | 2 +- .../actions/push_notifications/registerer.js | 1 - .../flavours/glitch/actions/reports.js | 10 +- .../flavours/glitch/actions/search.js | 1 - .../flavours/glitch/actions/server.js | 28 - .../flavours/glitch/actions/settings.js | 4 +- .../flavours/glitch/actions/statuses.js | 7 +- .../flavours/glitch/actions/store.js | 1 - .../flavours/glitch/actions/streaming.js | 61 +- .../flavours/glitch/actions/suggestions.js | 3 +- .../flavours/glitch/actions/timelines.js | 21 +- .../flavours/glitch/actions/trends.js | 1 - app/javascript/flavours/glitch/api.js | 6 +- .../flavours/glitch/base_polyfills.js | 47 + app/javascript/flavours/glitch/blurhash.js | 112 + app/javascript/flavours/glitch/compare_id.js | 11 + .../flavours/glitch/components/account.jsx | 109 +- .../glitch/components/admin/Counter.jsx | 28 +- .../glitch/components/admin/Dimension.jsx | 10 +- .../components/admin/ReportReasonSelector.jsx | 22 +- .../glitch/components/admin/Retention.jsx | 11 +- .../glitch/components/admin/Trends.jsx | 11 +- .../glitch/components/animated_number.jsx | 76 + .../glitch/components/attachment_list.jsx | 13 +- .../glitch/components/autosuggest_emoji.jsx | 6 +- .../glitch/components/autosuggest_hashtag.jsx | 8 +- .../glitch/components/autosuggest_input.jsx | 19 +- .../components/autosuggest_textarea.jsx | 20 +- .../flavours/glitch/components/avatar.jsx | 79 + .../glitch/components/avatar_composite.jsx | 16 +- .../glitch/components/avatar_overlay.jsx | 6 +- .../flavours/glitch/components/blurhash.jsx | 65 + .../flavours/glitch/components/button.jsx | 5 +- .../flavours/glitch/components/check.jsx | 2 + .../flavours/glitch/components/column.jsx | 16 +- .../glitch/components/column_back_button.jsx | 29 +- .../components/column_back_button_slim.jsx | 30 +- .../glitch/components/column_header.jsx | 43 +- .../glitch/components/common_counter.jsx | 3 + .../glitch/components/dismissable_banner.jsx | 12 +- .../glitch/components/display_name.jsx | 102 + .../flavours/glitch/components/domain.jsx | 42 + .../glitch/components/dropdown_menu.jsx | 41 +- .../containers/dropdown_menu_container.js | 1 - .../components/edited_timestamp/index.jsx | 34 +- .../glitch/components/error_boundary.jsx | 16 +- .../flavours/glitch/components/gifv.jsx | 77 + .../flavours/glitch/components/hashtag.jsx | 30 +- .../flavours/glitch/components/icon.jsx | 21 + .../glitch/components/icon_button.jsx | 177 + .../glitch/components/icon_with_badge.jsx | 22 + .../flavours/glitch/components/image.jsx | 33 + .../glitch/components/inline_account.jsx | 11 +- .../intersection_observer_article.jsx | 12 +- .../flavours/glitch/components/link.jsx | 8 +- .../flavours/glitch/components/load_gap.jsx | 34 + .../flavours/glitch/components/load_more.jsx | 27 + .../glitch/components/load_pending.jsx | 22 + .../glitch/components/loading_indicator.jsx | 32 + .../flavours/glitch/components/logo.jsx | 12 +- .../glitch/components/media_attachments.jsx | 23 +- .../glitch/components/media_gallery.jsx | 112 +- .../glitch/components/missing_indicator.jsx | 29 + .../flavours/glitch/components/modal_root.jsx | 9 +- .../flavours/glitch/components/name_list.js | 117 + .../glitch/components/navigation_portal.jsx | 27 +- .../components/not_signed_in_indicator.jsx | 12 + .../components/notification_purge_buttons.jsx | 19 +- .../flavours/glitch/components/permalink.jsx | 8 +- .../picture_in_picture_placeholder.jsx | 58 +- .../flavours/glitch/components/poll.jsx | 47 +- .../glitch/components/radio_button.jsx | 35 + .../components/regeneration_indicator.jsx | 2 +- .../glitch/components/relative_timestamp.jsx | 199 + .../glitch/components/scrollable_list.jsx | 97 +- .../glitch/components/server_banner.jsx | 22 +- .../glitch/components/setting_text.jsx | 5 +- .../glitch/components/short_number.jsx | 13 +- .../flavours/glitch/components/skeleton.jsx | 11 + .../flavours/glitch/components/spoilers.jsx | 52 + .../flavours/glitch/components/status.jsx | 150 +- .../glitch/components/status_action_bar.jsx | 74 +- .../glitch/components/status_content.jsx | 92 +- .../glitch/components/status_header.jsx | 9 +- .../glitch/components/status_icons.jsx | 40 +- .../glitch/components/status_list.jsx | 19 +- .../glitch/components/status_prepend.jsx | 14 +- .../glitch/components/status_reactions.js | 170 + .../components/status_visibility_icon.jsx | 11 +- .../glitch/components/timeline_hint.jsx | 18 + .../glitch/containers/account_container.jsx | 20 +- .../glitch/containers/admin_component.jsx | 14 +- .../glitch/containers/compose_container.jsx | 27 +- .../glitch/containers/domain_container.jsx | 21 +- .../containers/dropdown_menu_container.js | 20 +- ...intersection_observer_article_container.js | 3 +- .../flavours/glitch/containers/mastodon.jsx | 31 +- .../glitch/containers/media_container.jsx | 54 +- .../notification_purge_buttons_container.js | 18 +- .../glitch/containers/poll_container.js | 3 +- .../glitch/containers/status_container.jsx | 138 +- .../flavours/glitch/extra_polyfills.js | 2 + .../flavours/glitch/features/about/index.jsx | 36 +- .../account/components/account_note.jsx | 20 +- .../account/components/action_bar.jsx | 24 +- .../account/components/featured_tags.jsx | 11 +- .../components/follow_request_note.jsx | 7 +- .../features/account/components/header.jsx | 53 +- .../components/profile_column_header.jsx | 11 +- .../containers/account_note_container.js | 2 - .../containers/featured_tags_container.js | 6 +- .../follow_request_note_container.js | 4 +- .../glitch/features/account/navigation.jsx | 9 +- .../account_gallery/components/media_item.jsx | 14 +- .../glitch/features/account_gallery/index.jsx | 55 +- .../account_timeline/components/header.jsx | 20 +- .../components/limited_account_hint.jsx | 12 +- .../components/moved_note.jsx | 17 +- .../containers/header_container.jsx | 91 +- .../features/account_timeline/index.jsx | 54 +- .../flavours/glitch/features/audio/index.jsx | 56 +- .../flavours/glitch/features/blocks/index.jsx | 30 +- .../features/bookmarked_statuses/index.jsx | 16 +- .../closed_registrations_modal/index.jsx | 12 +- .../components/column_settings.jsx | 12 +- .../containers/column_settings_container.js | 4 +- .../features/community_timeline/index.jsx | 31 +- .../compose/components/action_bar.jsx | 19 +- .../components/autosuggest_account.jsx | 6 +- .../compose/components/character_counter.jsx | 5 +- .../compose/components/compose_form.jsx | 54 +- .../features/compose/components/dropdown.jsx | 20 +- .../compose/components/dropdown_menu.jsx | 35 +- .../components/emoji_picker_dropdown.jsx | 44 +- .../features/compose/components/header.jsx | 24 +- .../compose/components/language_dropdown.jsx | 31 +- .../compose/components/navigation_bar.jsx | 15 +- .../features/compose/components/options.jsx | 45 +- .../features/compose/components/poll_form.jsx | 24 +- .../compose/components/privacy_dropdown.jsx | 12 +- .../features/compose/components/publisher.jsx | 36 +- .../compose/components/quote_indicator.js | 86 + .../compose/components/reply_indicator.jsx | 17 +- .../features/compose/components/search.jsx | 23 +- .../compose/components/search_results.jsx | 19 +- .../compose/components/text_icon_button.jsx | 4 +- .../compose/components/textarea_icons.jsx | 11 +- .../features/compose/components/upload.jsx | 19 +- .../compose/components/upload_form.jsx | 8 +- .../compose/components/upload_progress.jsx | 14 +- .../features/compose/components/warning.jsx | 8 +- .../autosuggest_account_container.js | 4 +- .../containers/compose_form_container.js | 38 +- .../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 | 11 +- .../compose/containers/poll_form_container.js | 4 +- .../containers/privacy_dropdown_container.js | 14 +- .../containers/quote_indicator_container.js | 27 + .../containers/reply_indicator_container.js | 2 - .../compose/containers/search_container.js | 2 - .../containers/search_results_container.js | 6 +- .../containers/sensitive_button_container.jsx | 14 +- .../compose/containers/upload_container.js | 4 +- .../containers/upload_form_container.js | 1 - .../containers/upload_progress_container.js | 1 - .../compose/containers/warning_container.jsx | 15 +- .../glitch/features/compose/index.jsx | 40 +- .../glitch/features/compose/util/url_regex.js | 2 +- .../components/column_settings.jsx | 13 +- .../components/conversation.jsx | 33 +- .../components/conversations_list.jsx | 13 +- .../containers/column_settings_container.js | 4 +- .../containers/conversation_container.js | 22 +- .../conversations_list_container.js | 4 +- .../glitch/features/direct_timeline/index.jsx | 25 +- .../directory/components/account_card.jsx | 69 +- .../glitch/features/directory/index.jsx | 35 +- .../glitch/features/domain_blocks/index.jsx | 37 +- .../flavours/glitch/features/emoji/emoji.js | 100 +- .../glitch/features/emoji/emoji_compressed.js | 21 +- .../features/emoji/emoji_mart_data_light.js | 41 + .../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 | 2 +- .../features/emoji/unicode_to_filename.js | 3 - .../features/emoji/unicode_to_unified_name.js | 3 - .../features/explore/components/story.jsx | 17 +- .../glitch/features/explore/index.jsx | 48 +- .../glitch/features/explore/links.jsx | 21 +- .../glitch/features/explore/results.jsx | 32 +- .../glitch/features/explore/statuses.jsx | 21 +- .../glitch/features/explore/suggestions.jsx | 18 +- .../flavours/glitch/features/explore/tags.jsx | 22 +- .../features/favourited_statuses/index.jsx | 16 +- .../glitch/features/favourites/index.jsx | 26 +- .../features/filters/added_to_filter.jsx | 28 +- .../glitch/features/filters/select_filter.jsx | 26 +- .../components/account.jsx | 20 +- .../features/follow_recommendations/index.jsx | 27 +- .../components/account_authorize.jsx | 17 +- .../containers/account_authorize_container.js | 4 +- .../glitch/features/follow_requests/index.jsx | 36 +- .../glitch/features/followed_tags/index.jsx | 26 +- .../glitch/features/followers/index.jsx | 37 +- .../glitch/features/following/index.jsx | 37 +- .../features/generic_not_found/index.jsx | 11 + .../components/announcements.jsx | 60 +- .../getting_started/components/trends.jsx | 13 +- .../containers/announcements_container.js | 6 +- .../containers/trends_container.js | 2 - .../glitch/features/getting_started/index.jsx | 54 +- .../features/getting_started_misc/index.jsx | 62 +- .../components/column_settings.jsx | 17 +- .../containers/column_settings_container.js | 4 +- .../features/hashtag_timeline/index.jsx | 45 +- .../components/column_settings.jsx | 16 +- .../containers/column_settings_container.js | 4 +- .../glitch/features/home_timeline/index.jsx | 36 +- .../features/interaction_modal/index.jsx | 36 +- .../features/keyboard_shortcuts/index.jsx | 20 +- .../list_adder/components/account.jsx | 18 +- .../features/list_adder/components/list.jsx | 21 +- .../glitch/features/list_adder/index.jsx | 19 +- .../list_editor/components/account.jsx | 15 +- .../list_editor/components/edit_list_form.jsx | 17 +- .../list_editor/components/search.jsx | 11 +- .../containers/account_container.js | 8 +- .../containers/search_container.js | 5 +- .../glitch/features/list_editor/index.jsx | 24 +- .../glitch/features/list_timeline/index.jsx | 85 +- .../lists/components/new_list_form.jsx | 17 +- .../flavours/glitch/features/lists/index.jsx | 16 +- .../glitch/features/local_settings/index.jsx | 17 +- .../local_settings/navigation/index.jsx | 11 +- .../local_settings/navigation/item/index.jsx | 14 +- .../page/deprecated_item/index.jsx | 6 +- .../features/local_settings/page/index.jsx | 29 +- .../local_settings/page/item/index.jsx | 9 +- .../flavours/glitch/features/mutes/index.jsx | 35 +- .../notifications/components/admin_report.jsx | 31 +- .../notifications/components/admin_signup.jsx | 17 +- .../components/clear_column_button.jsx | 10 +- .../components/column_settings.jsx | 14 +- .../notifications/components/filter_bar.jsx | 11 +- .../notifications/components/follow.jsx | 17 +- .../components/follow_request.jsx | 34 +- .../components/grant_permission_button.jsx | 7 +- .../notifications/components/notification.jsx | 10 +- .../notifications_permission_banner.jsx | 25 +- .../notifications/components/overlay.jsx | 13 +- .../components/pill_bar_button.jsx | 8 +- .../notifications/components/report.jsx | 21 +- .../components/setting_toggle.jsx | 6 +- .../containers/admin_report_container.js | 2 - .../containers/column_settings_container.js | 24 +- .../containers/filter_bar_container.js | 3 +- .../containers/follow_request_container.js | 5 +- .../containers/notification_container.js | 3 +- .../containers/overlay_container.js | 3 +- .../glitch/features/notifications/index.jsx | 111 +- .../picture_in_picture/components/footer.jsx | 71 +- .../picture_in_picture/components/header.jsx | 27 +- .../features/picture_in_picture/index.jsx | 21 +- .../containers/account_container.js | 7 +- .../containers/search_container.js | 9 +- .../features/pinned_accounts_editor/index.jsx | 20 +- .../glitch/features/pinned_statuses/index.jsx | 23 +- .../glitch/features/privacy_policy/index.jsx | 16 +- .../components/column_settings.jsx | 12 +- .../containers/column_settings_container.js | 6 +- .../glitch/features/public_timeline/index.jsx | 29 +- .../glitch/features/reblogs/index.jsx | 35 +- .../glitch/features/report/category.jsx | 20 +- .../glitch/features/report/comment.jsx | 20 +- .../features/report/components/option.jsx | 8 +- .../report/components/status_check_box.jsx | 17 +- .../containers/status_check_box_container.js | 4 +- .../flavours/glitch/features/report/rules.jsx | 17 +- .../glitch/features/report/statuses.jsx | 23 +- .../glitch/features/report/thanks.jsx | 22 +- .../features/standalone/compose/index.jsx | 7 +- .../features/status/components/action_bar.jsx | 49 +- .../features/status/components/card.jsx | 84 +- .../status/components/detailed_status.jsx | 100 +- .../containers/detailed_status_container.js | 86 +- .../flavours/glitch/features/status/index.jsx | 239 +- .../subscribed_languages_modal/index.jsx | 22 +- .../features/ui/components/actions_modal.jsx | 20 +- .../features/ui/components/audio_modal.jsx | 24 +- .../features/ui/components/block_modal.jsx | 28 +- .../features/ui/components/boost_modal.jsx | 37 +- .../glitch/features/ui/components/bundle.jsx | 8 +- .../ui/components/bundle_column_error.jsx | 20 +- .../ui/components/bundle_modal_error.jsx | 7 +- .../glitch/features/ui/components/column.jsx | 15 +- .../features/ui/components/column_header.jsx | 8 +- .../features/ui/components/column_link.jsx | 10 +- .../features/ui/components/column_loading.jsx | 4 +- .../ui/components/column_subheading.jsx | 1 + .../features/ui/components/columns_area.jsx | 25 +- .../ui/components/compare_history_modal.jsx | 32 +- .../features/ui/components/compose_panel.jsx | 27 +- .../ui/components/confirmation_modal.jsx | 9 +- .../components/deprecated_settings_modal.jsx | 20 +- .../ui/components/disabled_account_banner.jsx | 31 +- .../features/ui/components/doodle_modal.jsx | 53 +- .../features/ui/components/drawer_loading.jsx | 2 + .../features/ui/components/embed_modal.jsx | 13 +- .../ui/components/favourite_modal.jsx | 30 +- .../features/ui/components/filter_modal.jsx | 20 +- .../ui/components/focal_point_modal.jsx | 52 +- .../follow_requests_column_link.jsx | 19 +- .../glitch/features/ui/components/header.jsx | 46 +- .../features/ui/components/image_loader.jsx | 7 +- .../features/ui/components/image_modal.jsx | 15 +- .../features/ui/components/link_footer.jsx | 38 +- .../features/ui/components/list_panel.jsx | 13 +- .../features/ui/components/media_modal.jsx | 60 +- .../features/ui/components/modal_loading.jsx | 4 +- .../features/ui/components/modal_root.jsx | 44 +- .../features/ui/components/mute_modal.jsx | 27 +- .../ui/components/navigation_panel.jsx | 28 +- .../components/notifications_counter_icon.js | 3 +- .../ui/components/onboarding_modal.jsx | 42 +- .../features/ui/components/report_modal.jsx | 34 +- .../features/ui/components/sign_in_banner.jsx | 22 +- .../features/ui/components/upload_area.jsx | 13 +- .../features/ui/components/video_modal.jsx | 34 +- .../features/ui/components/zoomable_image.jsx | 15 +- .../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 | 11 +- .../flavours/glitch/features/ui/index.jsx | 107 +- .../features/ui/util/async-components.js | 4 + .../features/ui/util/optional_motion.js | 6 +- .../features/ui/util/react_router_helpers.jsx | 7 +- .../features/ui/util/reduced_motion.jsx | 7 +- .../flavours/glitch/features/video/index.jsx | 114 +- .../flavours/glitch/initial_state.js | 24 +- app/javascript/flavours/glitch/is_mobile.js | 55 + .../flavours/glitch/load_polyfills.js | 42 + .../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 | 21 +- .../glitch/locales/defaultMessages.json | 1068 ++++ .../flavours/glitch/locales/el.json | 4 - .../flavours/glitch/locales/en-GB.json | 11 +- .../flavours/glitch/locales/en.json | 25 +- .../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 | 136 +- .../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 | 11 +- .../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/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/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 | 7 +- .../flavours/glitch/locales/ku.json | 4 - .../flavours/glitch/locales/kw.json | 4 - .../flavours/glitch/locales/la.json | 11 +- .../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 | 7 +- .../flavours/glitch/locales/pt-PT.json | 4 - .../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 - app/javascript/flavours/glitch/main.jsx | 13 +- .../flavours/glitch/middleware/errors.js | 17 + .../flavours/glitch/middleware/loading_bar.js | 25 + .../flavours/glitch/middleware/sounds.js | 46 + .../flavours/glitch/packs/admin.jsx | 17 +- .../flavours/glitch/packs/common.js | 3 +- app/javascript/flavours/glitch/packs/home.js | 17 +- .../flavours/glitch/packs/public.jsx | 85 +- .../flavours/glitch/packs/settings.js | 11 +- .../flavours/glitch/packs/share.jsx | 20 +- app/javascript/flavours/glitch/performance.js | 5 +- app/javascript/flavours/glitch/permissions.js | 4 + .../flavours/glitch/reducers/accounts.js | 5 +- .../glitch/reducers/accounts_counters.js | 3 +- .../flavours/glitch/reducers/accounts_map.js | 5 +- .../flavours/glitch/reducers/alerts.js | 3 +- .../flavours/glitch/reducers/announcements.js | 3 +- .../flavours/glitch/reducers/compose.js | 52 +- .../flavours/glitch/reducers/contexts.js | 6 +- .../flavours/glitch/reducers/conversations.js | 8 +- .../flavours/glitch/reducers/custom_emojis.js | 3 +- .../flavours/glitch/reducers/domain_lists.js | 3 +- .../flavours/glitch/reducers/dropdown_menu.js | 1 - .../flavours/glitch/reducers/filters.js | 5 +- .../flavours/glitch/reducers/followed_tags.js | 3 +- .../flavours/glitch/reducers/height_cache.js | 1 - .../flavours/glitch/reducers/history.js | 3 +- .../flavours/glitch/reducers/index.js | 94 + .../flavours/glitch/reducers/list_adder.js | 1 - .../flavours/glitch/reducers/list_editor.js | 3 - .../flavours/glitch/reducers/lists.js | 3 +- .../glitch/reducers/local_settings.js | 3 +- .../flavours/glitch/reducers/markers.js | 5 +- .../glitch/reducers/media_attachments.js | 3 +- .../flavours/glitch/reducers/meta.js | 9 +- .../flavours/glitch/reducers/modal.js | 39 + .../flavours/glitch/reducers/notifications.js | 26 +- .../glitch/reducers/pinned_accounts_editor.js | 7 +- .../flavours/glitch/reducers/polls.js | 32 +- .../glitch/reducers/push_notifications.js | 5 +- .../flavours/glitch/reducers/relationships.js | 13 +- .../flavours/glitch/reducers/search.js | 13 +- .../flavours/glitch/reducers/server.js | 12 +- .../flavours/glitch/reducers/settings.js | 12 +- .../flavours/glitch/reducers/status_lists.js | 43 +- .../flavours/glitch/reducers/statuses.js | 30 +- .../flavours/glitch/reducers/suggestions.js | 9 +- .../flavours/glitch/reducers/tags.js | 3 +- .../flavours/glitch/reducers/timelines.js | 16 +- .../flavours/glitch/reducers/trends.js | 3 +- .../flavours/glitch/reducers/user_lists.js | 35 +- app/javascript/flavours/glitch/scroll.js | 32 + .../flavours/glitch/selectors/index.js | 14 +- .../flavours/glitch/store/configureStore.js | 15 + app/javascript/flavours/glitch/stream.js | 25 +- .../flavours/glitch/styles/_mixins.scss | 1 + .../flavours/glitch/styles/about.scss | 4 +- .../flavours/glitch/styles/accessibility.scss | 6 +- .../flavours/glitch/styles/accounts.scss | 34 +- .../flavours/glitch/styles/admin.scss | 106 +- .../flavours/glitch/styles/basics.scss | 2 +- .../flavours/glitch/styles/bird.scss | 3 + .../flavours/glitch/styles/bird/diff.scss | 3844 ++++++++++++++ .../glitch/styles/bird/variables.scss | 161 + .../glitch/styles/components/about.scss | 6 +- .../glitch/styles/components/accounts.scss | 76 +- .../styles/components/announcements.scss | 10 +- .../glitch/styles/components/columns.scss | 97 +- .../styles/components/compose_form.scss | 33 +- .../glitch/styles/components/directory.scss | 2 +- .../glitch/styles/components/doodle.scss | 14 +- .../glitch/styles/components/drawer.scss | 30 +- .../glitch/styles/components/emoji.scss | 9 +- .../styles/components/emoji_picker.scss | 10 +- .../styles/components/error_boundary.scss | 4 +- .../glitch/styles/components/explore.scss | 4 +- .../glitch/styles/components/lists.scss | 4 +- .../styles/components/local_settings.scss | 11 +- .../glitch/styles/components/media.scss | 102 +- .../glitch/styles/components/misc.scss | 256 +- .../glitch/styles/components/modal.scss | 76 +- .../styles/components/privacy_policy.scss | 8 +- .../glitch/styles/components/search.scss | 10 +- .../glitch/styles/components/sensitive.scss | 2 +- .../glitch/styles/components/signed_out.scss | 2 +- .../styles/components/single_column.scss | 13 +- .../glitch/styles/components/status.scss | 184 +- .../flavours/glitch/styles/containers.scss | 12 +- .../flavours/glitch/styles/contrast/diff.scss | 1 + .../flavours/glitch/styles/dashboard.scss | 2 +- .../flavours/glitch/styles/forms.scss | 133 +- .../glitch/styles/mastodon-light/diff.scss | 5 +- .../flavours/glitch/styles/modal.scss | 4 +- .../flavours/glitch/styles/polls.scss | 2 +- .../flavours/glitch/styles/rich_text.scss | 8 +- .../flavours/glitch/styles/rtl.scss | 267 +- .../flavours/glitch/styles/statuses.scss | 13 +- .../flavours/glitch/styles/tables.scss | 24 +- .../flavours/glitch/styles/variables.scss | 7 - .../flavours/glitch/styles/widgets.scss | 8 +- app/javascript/flavours/glitch/theme.yml | 1 - .../flavours/glitch/utils/base64.js | 10 + .../flavours/glitch/utils/dom_helpers.js | 7 + .../flavours/glitch/utils/filters.js | 16 + .../flavours/glitch/utils/hashtag.js | 2 +- .../flavours/glitch/utils/icons.jsx | 4 + .../flavours/glitch/utils/log_out.js | 1 - .../flavours/glitch/utils/numbers.js | 79 + .../flavours/glitch/utils/resize_image.js | 189 + .../flavours/glitch/utils/scrollbar.js | 4 +- app/javascript/flavours/glitch/uuid.js | 3 + app/javascript/flavours/vanilla/theme.yml | 1 - app/javascript/icons/favicon-16x16.png | Bin 526 -> 588 bytes app/javascript/icons/favicon-32x32.png | Bin 1265 -> 1114 bytes app/javascript/icons/favicon-48x48.png | Bin 1681 -> 1680 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/blocks.js | 3 +- app/javascript/mastodon/actions/bookmarks.js | 1 - app/javascript/mastodon/actions/boosts.js | 5 +- app/javascript/mastodon/actions/compose.js | 95 +- .../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/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 | 5 +- app/javascript/mastodon/actions/reports.js | 10 +- app/javascript/mastodon/actions/search.js | 51 +- app/javascript/mastodon/actions/server.js | 1 - 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/timelines.js | 10 +- app/javascript/mastodon/actions/trends.js | 1 - app/javascript/mastodon/api.js | 5 +- app/javascript/mastodon/base_polyfills.js | 47 + app/javascript/mastodon/blurhash.js | 112 + app/javascript/mastodon/common.js | 2 +- app/javascript/mastodon/compare_id.js | 11 + .../avatar_overlay-test.jsx.snap | 6 +- .../__tests__/autosuggest_emoji-test.jsx | 2 +- .../components/__tests__/avatar-test.jsx | 7 +- .../__tests__/avatar_overlay-test.jsx | 7 +- .../components/__tests__/button-test.jsx | 2 +- .../__tests__/display_name-test.jsx | 7 +- .../mastodon/components/account.jsx | 131 +- .../mastodon/components/admin/Counter.jsx | 28 +- .../mastodon/components/admin/Dimension.jsx | 10 +- .../components/admin/ReportReasonSelector.jsx | 22 +- .../mastodon/components/admin/Retention.jsx | 11 +- .../mastodon/components/admin/Trends.jsx | 11 +- .../mastodon/components/animated_number.jsx | 76 + .../mastodon/components/attachment_list.jsx | 13 +- .../mastodon/components/autosuggest_emoji.jsx | 8 +- .../components/autosuggest_hashtag.jsx | 8 +- .../mastodon/components/autosuggest_input.jsx | 17 +- .../components/autosuggest_textarea.jsx | 20 +- app/javascript/mastodon/components/avatar.jsx | 62 + .../mastodon/components/avatar_composite.jsx | 9 +- .../mastodon/components/avatar_overlay.jsx | 51 + .../mastodon/components/blurhash.jsx | 65 + app/javascript/mastodon/components/button.jsx | 5 +- app/javascript/mastodon/components/check.jsx | 9 + app/javascript/mastodon/components/column.jsx | 16 +- .../components/column_back_button.jsx | 24 +- .../components/column_back_button_slim.jsx | 7 +- .../mastodon/components/column_header.jsx | 14 +- .../mastodon/components/common_counter.jsx | 2 + .../components/dismissable_banner.jsx | 12 +- .../mastodon/components/display_name.jsx | 79 + app/javascript/mastodon/components/domain.jsx | 42 + .../mastodon/components/dropdown_menu.jsx | 41 +- .../containers/dropdown_menu_container.js | 1 - .../components/edited_timestamp/index.jsx | 34 +- .../mastodon/components/error_boundary.jsx | 12 +- app/javascript/mastodon/components/gifv.jsx | 76 + .../mastodon/components/hashtag.jsx | 33 +- app/javascript/mastodon/components/icon.jsx | 21 + .../mastodon/components/icon_button.jsx | 164 + .../mastodon/components/icon_with_badge.jsx | 22 + app/javascript/mastodon/components/image.jsx | 33 + .../mastodon/components/inline_account.jsx | 11 +- .../intersection_observer_article.jsx | 15 +- .../mastodon/components/load_gap.jsx | 34 + .../mastodon/components/load_more.jsx | 27 + .../mastodon/components/load_pending.jsx | 22 + .../mastodon/components/loading_indicator.jsx | 32 + app/javascript/mastodon/components/logo.jsx | 10 + .../mastodon/components/media_attachments.jsx | 23 +- .../mastodon/components/media_gallery.jsx | 111 +- .../mastodon/components/missing_indicator.jsx | 29 + .../mastodon/components/modal_root.jsx | 9 +- .../mastodon/components/navigation_portal.jsx | 26 +- .../components/not_signed_in_indicator.jsx | 12 + .../picture_in_picture_placeholder.jsx | 58 +- app/javascript/mastodon/components/poll.jsx | 46 +- .../mastodon/components/radio_button.jsx | 35 + .../components/regeneration_indicator.jsx | 2 +- .../components/relative_timestamp.jsx | 199 + .../mastodon/components/scrollable_list.jsx | 50 +- .../mastodon/components/server_banner.jsx | 22 +- .../mastodon/components/short_number.jsx | 14 +- .../mastodon/components/skeleton.jsx | 11 + app/javascript/mastodon/components/status.jsx | 119 +- .../mastodon/components/status_action_bar.jsx | 58 +- .../mastodon/components/status_content.jsx | 58 +- .../mastodon/components/status_list.jsx | 20 +- .../mastodon/components/status_reactions.js | 170 + .../mastodon/components/timeline_hint.jsx | 18 + .../mastodon/containers/account_container.jsx | 20 +- .../mastodon/containers/admin_component.jsx | 14 +- .../mastodon/containers/compose_container.jsx | 26 +- .../mastodon/containers/domain_container.jsx | 18 +- .../containers/dropdown_menu_container.js | 22 +- ...intersection_observer_article_container.js | 3 +- .../mastodon/containers/mastodon.jsx | 29 +- .../mastodon/containers/media_container.jsx | 54 +- .../mastodon/containers/poll_container.js | 3 +- .../mastodon/containers/status_container.jsx | 128 +- app/javascript/mastodon/extra_polyfills.js | 2 + .../mastodon/features/about/index.jsx | 38 +- .../account/components/account_note.jsx | 22 +- .../account/components/featured_tags.jsx | 9 +- .../components/follow_request_note.jsx | 7 +- .../features/account/components/header.jsx | 121 +- .../containers/account_note_container.js | 2 - .../containers/featured_tags_container.js | 6 +- .../follow_request_note_container.js | 4 +- .../mastodon/features/account/navigation.jsx | 9 +- .../account_gallery/components/media_item.jsx | 14 +- .../features/account_gallery/index.jsx | 55 +- .../account_timeline/components/header.jsx | 18 +- .../components/limited_account_hint.jsx | 12 +- .../components/moved_note.jsx | 12 +- .../containers/header_container.jsx | 90 +- .../features/account_timeline/index.jsx | 45 +- .../mastodon/features/audio/index.jsx | 53 +- .../mastodon/features/blocks/index.jsx | 29 +- .../features/bookmarked_statuses/index.jsx | 16 +- .../closed_registrations_modal/index.jsx | 12 +- .../components/column_settings.jsx | 12 +- .../containers/column_settings_container.js | 5 +- .../features/community_timeline/index.jsx | 32 +- .../compose/components/action_bar.jsx | 16 +- .../components/autosuggest_account.jsx | 6 +- .../compose/components/character_counter.jsx | 5 +- .../compose/components/compose_form.jsx | 121 +- .../components/emoji_picker_dropdown.jsx | 46 +- .../compose/components/language_dropdown.jsx | 31 +- .../compose/components/navigation_bar.jsx | 16 +- .../compose/components/poll_button.jsx | 12 +- .../features/compose/components/poll_form.jsx | 25 +- .../compose/components/privacy_dropdown.jsx | 33 +- .../compose/components/reply_indicator.jsx | 18 +- .../features/compose/components/search.jsx | 306 +- .../compose/components/search_results.jsx | 22 +- .../compose/components/text_icon_button.jsx | 4 +- .../features/compose/components/upload.jsx | 18 +- .../compose/components/upload_button.jsx | 14 +- .../compose/components/upload_form.jsx | 8 +- .../compose/components/upload_progress.jsx | 14 +- .../features/compose/components/warning.jsx | 8 +- .../autosuggest_account_container.js | 3 +- .../containers/compose_form_container.js | 3 +- .../emoji_picker_dropdown_container.js | 10 +- .../containers/language_dropdown_container.js | 9 +- .../containers/navigation_container.js | 24 +- .../containers/poll_button_container.js | 3 +- .../compose/containers/poll_form_container.js | 3 +- .../containers/privacy_dropdown_container.js | 13 +- .../containers/reply_indicator_container.js | 1 - .../compose/containers/search_container.js | 24 +- .../containers/search_results_container.js | 6 +- .../containers/sensitive_button_container.jsx | 14 +- .../containers/spoiler_button_container.js | 6 +- .../containers/upload_button_container.js | 3 +- .../compose/containers/upload_container.js | 3 +- .../containers/upload_form_container.js | 1 - .../containers/upload_progress_container.js | 1 - .../compose/containers/warning_container.jsx | 39 +- .../mastodon/features/compose/index.jsx | 60 +- .../features/compose/util/url_regex.js | 2 +- .../components/conversation.jsx | 29 +- .../components/conversations_list.jsx | 12 +- .../containers/conversation_container.js | 22 +- .../conversations_list_container.js | 3 +- .../features/direct_timeline/index.jsx | 21 +- .../directory/components/account_card.jsx | 67 +- .../mastodon/features/directory/index.jsx | 35 +- .../mastodon/features/domain_blocks/index.jsx | 32 +- .../features/emoji/__tests__/emoji-test.js | 4 +- .../emoji/__tests__/emoji_index-test.js | 3 +- .../mastodon/features/emoji/emoji.js | 101 +- .../features/emoji/emoji_compressed.js | 21 +- .../features/emoji/emoji_mart_data_light.js | 41 + .../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 | 2 +- .../features/emoji/unicode_to_filename.js | 3 - .../features/emoji/unicode_to_unified_name.js | 3 - .../features/explore/components/story.jsx | 16 +- .../mastodon/features/explore/index.jsx | 46 +- .../mastodon/features/explore/links.jsx | 21 +- .../mastodon/features/explore/results.jsx | 30 +- .../mastodon/features/explore/statuses.jsx | 21 +- .../mastodon/features/explore/suggestions.jsx | 18 +- .../mastodon/features/explore/tags.jsx | 20 +- .../features/favourited_statuses/index.jsx | 16 +- .../mastodon/features/favourites/index.jsx | 25 +- .../features/filters/added_to_filter.jsx | 28 +- .../features/filters/select_filter.jsx | 26 +- .../components/account.jsx | 85 + .../features/follow_recommendations/index.jsx | 116 + .../components/account_authorize.jsx | 18 +- .../containers/account_authorize_container.js | 3 +- .../features/follow_requests/index.jsx | 29 +- .../mastodon/features/followed_tags/index.jsx | 26 +- .../mastodon/features/followers/index.jsx | 39 +- .../mastodon/features/following/index.jsx | 39 +- .../features/generic_not_found/index.jsx | 11 + .../components/announcements.jsx | 59 +- .../getting_started/components/trends.jsx | 13 +- .../containers/announcements_container.js | 6 +- .../containers/trends_container.js | 2 - .../features/getting_started/index.jsx | 37 +- .../components/column_settings.jsx | 17 +- .../containers/column_settings_container.js | 3 +- .../features/hashtag_timeline/index.jsx | 45 +- .../components/column_settings.jsx | 12 +- .../containers/column_settings_container.js | 3 +- .../mastodon/features/home_timeline/index.jsx | 36 +- .../features/interaction_modal/index.jsx | 36 +- .../features/keyboard_shortcuts/index.jsx | 16 +- .../list_adder/components/account.jsx | 18 +- .../features/list_adder/components/list.jsx | 19 +- .../mastodon/features/list_adder/index.jsx | 19 +- .../list_editor/components/account.jsx | 22 +- .../list_editor/components/edit_list_form.jsx | 17 +- .../list_editor/components/search.jsx | 21 +- .../mastodon/features/list_editor/index.jsx | 22 +- .../mastodon/features/list_timeline/index.jsx | 80 +- .../lists/components/new_list_form.jsx | 15 +- .../mastodon/features/lists/index.jsx | 18 +- .../mastodon/features/mutes/index.jsx | 32 +- .../components/clear_column_button.jsx | 10 +- .../components/column_settings.jsx | 12 +- .../notifications/components/filter_bar.jsx | 11 +- .../components/follow_request.jsx | 24 +- .../components/grant_permission_button.jsx | 7 +- .../notifications/components/notification.jsx | 47 +- .../notifications_permission_banner.jsx | 23 +- .../notifications/components/report.jsx | 23 +- .../components/setting_toggle.jsx | 6 +- .../containers/column_settings_container.js | 23 +- .../containers/filter_bar_container.js | 3 +- .../containers/follow_request_container.js | 4 +- .../containers/notification_container.js | 5 +- .../mastodon/features/notifications/index.jsx | 53 +- .../picture_in_picture/components/footer.jsx | 71 +- .../picture_in_picture/components/header.jsx | 25 +- .../features/picture_in_picture/index.jsx | 18 +- .../features/pinned_statuses/index.jsx | 23 +- .../features/privacy_policy/index.jsx | 16 +- .../components/column_settings.jsx | 12 +- .../containers/column_settings_container.js | 5 +- .../features/public_timeline/index.jsx | 30 +- .../mastodon/features/reblogs/index.jsx | 32 +- .../mastodon/features/report/category.jsx | 24 +- .../mastodon/features/report/comment.jsx | 20 +- .../features/report/components/option.jsx | 10 +- .../report/components/status_check_box.jsx | 25 +- .../containers/status_check_box_container.js | 4 +- .../mastodon/features/report/rules.jsx | 17 +- .../mastodon/features/report/statuses.jsx | 22 +- .../mastodon/features/report/thanks.jsx | 22 +- .../features/standalone/compose/index.jsx | 7 +- .../features/status/components/action_bar.jsx | 60 +- .../features/status/components/card.jsx | 84 +- .../status/components/detailed_status.jsx | 88 +- .../containers/detailed_status_container.js | 65 +- .../mastodon/features/status/index.jsx | 232 +- .../subscribed_languages_modal/index.jsx | 22 +- .../ui/components/__tests__/column-test.jsx | 2 +- .../features/ui/components/actions_modal.jsx | 10 +- .../features/ui/components/audio_modal.jsx | 24 +- .../features/ui/components/block_modal.jsx | 29 +- .../features/ui/components/boost_modal.jsx | 38 +- .../features/ui/components/bundle.jsx | 8 +- .../ui/components/bundle_column_error.jsx | 20 +- .../ui/components/bundle_modal_error.jsx | 7 +- .../features/ui/components/column.jsx | 15 +- .../features/ui/components/column_header.jsx | 8 +- .../features/ui/components/column_link.jsx | 7 +- .../features/ui/components/column_loading.jsx | 4 +- .../ui/components/column_subheading.jsx | 1 + .../features/ui/components/columns_area.jsx | 22 +- .../ui/components/compare_history_modal.jsx | 32 +- .../features/ui/components/compose_panel.jsx | 26 +- .../ui/components/confirmation_modal.jsx | 9 +- .../ui/components/disabled_account_banner.jsx | 31 +- .../features/ui/components/drawer_loading.jsx | 2 + .../features/ui/components/embed_modal.jsx | 13 +- .../features/ui/components/filter_modal.jsx | 20 +- .../ui/components/focal_point_modal.jsx | 53 +- .../follow_requests_column_link.jsx | 19 +- .../features/ui/components/header.jsx | 44 +- .../features/ui/components/image_loader.jsx | 7 +- .../features/ui/components/image_modal.jsx | 15 +- .../features/ui/components/link_footer.jsx | 35 +- .../features/ui/components/list_panel.jsx | 13 +- .../features/ui/components/media_modal.jsx | 60 +- .../features/ui/components/modal_loading.jsx | 4 +- .../features/ui/components/modal_root.jsx | 38 +- .../features/ui/components/mute_modal.jsx | 27 +- .../ui/components/navigation_panel.jsx | 27 +- .../components/notifications_counter_icon.js | 3 +- .../features/ui/components/report_modal.jsx | 32 +- .../features/ui/components/sign_in_banner.jsx | 23 +- .../features/ui/components/upload_area.jsx | 11 +- .../features/ui/components/video_modal.jsx | 28 +- .../features/ui/components/zoomable_image.jsx | 15 +- .../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 | 11 +- app/javascript/mastodon/features/ui/index.jsx | 76 +- .../features/ui/util/async-components.js | 8 +- .../features/ui/util/optional_motion.js | 6 +- .../features/ui/util/react_router_helpers.jsx | 13 +- .../features/ui/util/reduced_motion.jsx | 7 +- .../mastodon/features/video/index.jsx | 77 +- app/javascript/mastodon/initial_state.js | 5 +- app/javascript/mastodon/is_mobile.js | 43 + app/javascript/mastodon/load_polyfills.js | 42 + app/javascript/mastodon/locales/af.json | 77 +- app/javascript/mastodon/locales/an.json | 71 +- app/javascript/mastodon/locales/ar.json | 71 +- app/javascript/mastodon/locales/ast.json | 71 +- app/javascript/mastodon/locales/be.json | 73 +- app/javascript/mastodon/locales/bg.json | 99 +- app/javascript/mastodon/locales/bn.json | 175 +- app/javascript/mastodon/locales/br.json | 77 +- app/javascript/mastodon/locales/bs.json | 77 +- app/javascript/mastodon/locales/ca.json | 71 +- app/javascript/mastodon/locales/ckb.json | 133 +- app/javascript/mastodon/locales/co.json | 75 +- app/javascript/mastodon/locales/cs.json | 75 +- app/javascript/mastodon/locales/csb.json | 661 +++ app/javascript/mastodon/locales/cy.json | 71 +- app/javascript/mastodon/locales/da.json | 71 +- app/javascript/mastodon/locales/de.json | 79 +- .../mastodon/locales/defaultMessages.json | 4346 ++++++++++++++++ app/javascript/mastodon/locales/el.json | 567 +- app/javascript/mastodon/locales/en-GB.json | 73 +- app/javascript/mastodon/locales/en.json | 100 +- app/javascript/mastodon/locales/eo.json | 71 +- app/javascript/mastodon/locales/es-AR.json | 71 +- app/javascript/mastodon/locales/es-MX.json | 71 +- app/javascript/mastodon/locales/es.json | 87 +- app/javascript/mastodon/locales/et.json | 79 +- app/javascript/mastodon/locales/eu.json | 71 +- app/javascript/mastodon/locales/fa.json | 85 +- app/javascript/mastodon/locales/fi.json | 179 +- app/javascript/mastodon/locales/fo.json | 73 +- app/javascript/mastodon/locales/fr-QC.json | 75 +- app/javascript/mastodon/locales/fr.json | 79 +- app/javascript/mastodon/locales/fy.json | 71 +- app/javascript/mastodon/locales/ga.json | 73 +- app/javascript/mastodon/locales/gd.json | 75 +- app/javascript/mastodon/locales/gl.json | 71 +- app/javascript/mastodon/locales/he.json | 89 +- app/javascript/mastodon/locales/hi.json | 79 +- app/javascript/mastodon/locales/hr.json | 77 +- app/javascript/mastodon/locales/hu.json | 315 +- app/javascript/mastodon/locales/hy.json | 75 +- app/javascript/mastodon/locales/id.json | 71 +- app/javascript/mastodon/locales/ig.json | 77 +- app/javascript/mastodon/locales/index.js | 1 + app/javascript/mastodon/locales/io.json | 71 +- app/javascript/mastodon/locales/is.json | 71 +- app/javascript/mastodon/locales/it.json | 71 +- app/javascript/mastodon/locales/ja.json | 75 +- app/javascript/mastodon/locales/ka.json | 77 +- app/javascript/mastodon/locales/kab.json | 73 +- app/javascript/mastodon/locales/kk.json | 75 +- app/javascript/mastodon/locales/kn.json | 77 +- app/javascript/mastodon/locales/ko.json | 91 +- app/javascript/mastodon/locales/ku.json | 85 +- app/javascript/mastodon/locales/kw.json | 75 +- app/javascript/mastodon/locales/la.json | 77 +- .../mastodon/locales/locale-data/co.js | 108 + .../mastodon/locales/locale-data/sa.js | 97 + app/javascript/mastodon/locales/lt.json | 77 +- app/javascript/mastodon/locales/lv.json | 71 +- app/javascript/mastodon/locales/mk.json | 77 +- app/javascript/mastodon/locales/ml.json | 77 +- app/javascript/mastodon/locales/mr.json | 81 +- app/javascript/mastodon/locales/ms.json | 71 +- app/javascript/mastodon/locales/my.json | 111 +- app/javascript/mastodon/locales/nl.json | 77 +- app/javascript/mastodon/locales/nn.json | 71 +- app/javascript/mastodon/locales/no.json | 85 +- app/javascript/mastodon/locales/oc.json | 71 +- app/javascript/mastodon/locales/pa.json | 77 +- app/javascript/mastodon/locales/pl.json | 128 +- app/javascript/mastodon/locales/pt-BR.json | 71 +- app/javascript/mastodon/locales/pt-PT.json | 71 +- app/javascript/mastodon/locales/ro.json | 71 +- app/javascript/mastodon/locales/ru.json | 71 +- app/javascript/mastodon/locales/sa.json | 73 +- app/javascript/mastodon/locales/sc.json | 75 +- app/javascript/mastodon/locales/sco.json | 71 +- app/javascript/mastodon/locales/si.json | 73 +- app/javascript/mastodon/locales/sk.json | 77 +- app/javascript/mastodon/locales/sl.json | 71 +- app/javascript/mastodon/locales/sq.json | 71 +- app/javascript/mastodon/locales/sr-Latn.json | 73 +- app/javascript/mastodon/locales/sr.json | 83 +- app/javascript/mastodon/locales/sv.json | 95 +- app/javascript/mastodon/locales/szl.json | 77 +- app/javascript/mastodon/locales/ta.json | 75 +- app/javascript/mastodon/locales/tai.json | 77 +- app/javascript/mastodon/locales/te.json | 77 +- app/javascript/mastodon/locales/th.json | 85 +- app/javascript/mastodon/locales/tr.json | 75 +- app/javascript/mastodon/locales/tt.json | 79 +- app/javascript/mastodon/locales/ug.json | 77 +- app/javascript/mastodon/locales/uk.json | 71 +- app/javascript/mastodon/locales/ur.json | 77 +- app/javascript/mastodon/locales/uz.json | 77 +- app/javascript/mastodon/locales/vi.json | 63 +- .../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_csb.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_uz.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 | 77 +- app/javascript/mastodon/locales/zh-CN.json | 97 +- app/javascript/mastodon/locales/zh-HK.json | 75 +- app/javascript/mastodon/locales/zh-TW.json | 81 +- app/javascript/mastodon/main.jsx | 13 +- 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/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 | 12 +- 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 | 3 +- .../mastodon/reducers/height_cache.js | 1 - app/javascript/mastodon/reducers/history.js | 3 +- app/javascript/mastodon/reducers/index.js | 90 + .../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 + app/javascript/mastodon/reducers/modal.js | 39 + .../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 | 3 +- app/javascript/mastodon/reducers/settings.js | 11 +- .../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/selectors/index.js | 16 +- .../mastodon/service_worker/entry.js | 1 - .../service_worker/web_push_locales.js | 5 +- .../service_worker/web_push_notifications.js | 4 +- .../mastodon/store/configureStore.js | 15 + app/javascript/mastodon/stream.js | 48 +- .../mastodon/utils/__tests__/html-test.js | 2 +- app/javascript/mastodon/utils/base64.js | 10 + app/javascript/mastodon/utils/filters.js | 16 + app/javascript/mastodon/utils/icons.jsx | 2 + .../mastodon/utils/notifications.js | 2 +- app/javascript/mastodon/utils/numbers.js | 79 + app/javascript/mastodon/utils/resize_image.js | 189 + app/javascript/mastodon/utils/scrollbar.js | 4 +- app/javascript/mastodon/uuid.js | 3 + app/javascript/packs/admin.jsx | 17 +- app/javascript/packs/application.js | 18 +- app/javascript/packs/public.jsx | 96 +- app/javascript/packs/share.jsx | 20 +- .../skins/glitch/Mastodon-Bird/common.scss | 1 + .../skins/glitch/Mastodon-Bird/names.yml | 8 + .../skins/vanilla/contrast-modern/common.scss | 1 + .../skins/vanilla/contrast-modern/names.yml | 12 + .../vanilla/mastodon-birdsite/common.scss | 1 + .../skins/vanilla/mastodon-birdsite/names.yml | 12 + .../vanilla/mastodon-light-modern/common.scss | 1 + .../vanilla/mastodon-light-modern/names.yml | 12 + .../skins/vanilla/mastodon-modern/common.scss | 1 + .../skins/vanilla/mastodon-modern/names.yml | 12 + app/javascript/styles/application.scss | 3 +- app/javascript/styles/birdsite.css | 2524 +++++++++ app/javascript/styles/contrast-modern.scss | 4 + app/javascript/styles/fonts/roboto-mono.scss | 3 +- app/javascript/styles/mailer.scss | 2 +- app/javascript/styles/mastodon-birdsite.scss | 2 + .../styles/mastodon-light-modern.scss | 4 + .../styles/mastodon-light/diff.scss | 10 +- app/javascript/styles/mastodon-modern.scss | 2 + app/javascript/styles/mastodon/about.scss | 4 +- app/javascript/styles/mastodon/accounts.scss | 34 +- app/javascript/styles/mastodon/admin.scss | 92 +- app/javascript/styles/mastodon/basics.scss | 2 +- .../styles/mastodon/components.scss | 1316 ++--- .../styles/mastodon/containers.scss | 12 +- app/javascript/styles/mastodon/dashboard.scss | 2 +- .../styles/mastodon/emoji_picker.scss | 10 +- app/javascript/styles/mastodon/forms.scss | 136 +- app/javascript/styles/mastodon/modal.scss | 4 +- app/javascript/styles/mastodon/polls.scss | 2 +- app/javascript/styles/mastodon/rich_text.scss | 6 +- app/javascript/styles/mastodon/rtl.scss | 297 +- app/javascript/styles/mastodon/statuses.scss | 10 +- app/javascript/styles/mastodon/tables.scss | 24 +- app/javascript/styles/mastodon/variables.scss | 7 - app/javascript/styles/mastodon/widgets.scss | 16 +- app/javascript/styles/modern.scss | 1 + app/lib/account_reach_finder.rb | 9 +- app/lib/activity_tracker.rb | 2 +- app/lib/activitypub/activity.rb | 21 +- app/lib/activitypub/activity/announce.rb | 2 +- app/lib/activitypub/activity/create.rb | 33 +- app/lib/activitypub/activity/delete.rb | 8 +- app/lib/activitypub/activity/emoji_react.rb | 15 +- app/lib/activitypub/activity/flag.rb | 8 +- app/lib/activitypub/activity/like.rb | 18 +- app/lib/activitypub/activity/undo.rb | 13 +- app/lib/activitypub/activity/update.rb | 2 +- app/lib/activitypub/case_transform.rb | 4 +- app/lib/activitypub/dereferencer.rb | 11 +- app/lib/activitypub/tag_manager.rb | 12 +- 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 +- .../metrics/dimension/servers_dimension.rb | 24 +- .../metrics/dimension/sources_dimension.rb | 20 +- .../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 +- .../measure/resolved_reports_measure.rb | 16 +- .../metrics/measure/tag_servers_measure.rb | 24 +- app/lib/admin/metrics/retention.rb | 74 +- app/lib/advanced_text_formatter.rb | 1 - app/lib/application_extension.rb | 4 + .../connection_pool/shared_connection_pool.rb | 12 +- app/lib/emoji_formatter.rb | 13 +- app/lib/extractor.rb | 2 +- app/lib/feed_manager.rb | 42 +- app/lib/hash_object.rb | 10 + app/lib/importer/accounts_index_importer.rb | 4 +- app/lib/importer/tags_index_importer.rb | 4 +- app/lib/link_details_extractor.rb | 16 +- app/lib/permalink_redirector.rb | 48 +- app/lib/plain_text_formatter.rb | 2 +- app/lib/settings/extend.rb | 9 + app/lib/tag_manager.rb | 6 +- app/lib/text_formatter.rb | 2 +- app/lib/themes.rb | 47 +- app/lib/translation_service/deepl.rb | 19 +- .../translation_service/libre_translate.rb | 19 +- app/lib/user_settings_decorator.rb | 194 + app/lib/vacuum/access_tokens_vacuum.rb | 6 +- app/lib/webfinger_resource.rb | 4 +- app/mailers/notification_mailer.rb | 30 +- app/models/account.rb | 122 +- app/models/account_conversation.rb | 38 +- app/models/account_filter.rb | 2 +- app/models/account_migration.rb | 2 +- app/models/account_stat.rb | 2 +- app/models/account_statuses_cleanup_policy.rb | 30 +- app/models/account_statuses_filter.rb | 6 +- .../account_suggestions/setting_source.rb | 4 +- app/models/account_suggestions/source.rb | 4 +- app/models/admin/account_action.rb | 5 - app/models/admin/action_log.rb | 2 +- app/models/admin/appeal_filter.rb | 4 +- app/models/admin/status_filter.rb | 4 +- app/models/announcement_reaction.rb | 3 +- app/models/appeal.rb | 2 +- app/models/backup.rb | 2 +- app/models/block.rb | 2 +- app/models/concerns/account_associations.rb | 4 - app/models/concerns/account_interactions.rb | 40 +- app/models/concerns/attachmentable.rb | 4 +- app/models/concerns/ldap_authenticable.rb | 2 +- app/models/concerns/lockable.rb | 2 +- app/models/concerns/omniauthable.rb | 2 +- .../concerns/status_threading_concern.rb | 14 +- app/models/custom_emoji.rb | 3 +- app/models/custom_filter.rb | 2 +- app/models/direct_feed.rb | 9 +- app/models/domain_allow.rb | 2 +- app/models/domain_block.rb | 4 +- app/models/email_domain_block.rb | 2 +- app/models/follow_recommendation.rb | 2 +- app/models/follow_recommendation_filter.rb | 2 +- app/models/follow_request.rb | 3 +- app/models/form/account_batch.rb | 11 - app/models/form/admin_settings.rb | 1 - app/models/identity.rb | 2 +- app/models/import.rb | 4 +- app/models/instance.rb | 11 +- app/models/list.rb | 1 - app/models/list_account.rb | 25 +- app/models/media_attachment.rb | 24 +- app/models/notification.rb | 32 +- app/models/poll.rb | 2 +- app/models/preview_card.rb | 4 +- app/models/preview_card_provider.rb | 1 - app/models/relationship_filter.rb | 4 +- app/models/report.rb | 13 +- app/models/session_activation.rb | 2 +- app/models/site_upload.rb | 2 +- app/models/status.rb | 164 +- app/models/status_edit.rb | 10 +- app/models/status_reaction.rb | 4 +- app/models/tag.rb | 6 +- app/models/trends/history.rb | 2 +- app/models/trends/preview_card_filter.rb | 4 +- app/models/trends/status_filter.rb | 4 +- app/models/user.rb | 69 +- app/models/user_role.rb | 2 +- app/models/webhook.rb | 20 +- app/policies/status_policy.rb | 4 - app/presenters/instance_presenter.rb | 8 + .../status_relationships_presenter.rb | 4 +- .../activitypub/note_serializer.rb | 9 +- app/serializers/rest/account_serializer.rb | 2 +- .../rest/admin/webhook_event_serializer.rb | 2 - app/serializers/rest/instance_serializer.rb | 1 - app/serializers/rest/list_serializer.rb | 2 +- app/serializers/rest/mute_serializer.rb | 4 +- app/serializers/rest/status_serializer.rb | 14 +- .../rest/translation_serializer.rb | 35 +- .../fetch_featured_collection_service.rb | 4 +- .../fetch_featured_tags_collection_service.rb | 2 +- .../activitypub/fetch_remote_actor_service.rb | 2 +- .../fetch_remote_status_service.rb | 3 - .../activitypub/fetch_replies_service.rb | 4 +- ...epare_followers_synchronization_service.rb | 2 +- .../activitypub/process_account_service.rb | 2 +- .../process_status_update_service.rb | 4 +- .../synchronize_followers_service.rb | 2 +- app/services/app_sign_up_service.rb | 2 +- app/services/backup_service.rb | 152 +- app/services/fan_out_on_write_service.rb | 2 +- app/services/fetch_link_card_service.rb | 6 +- app/services/fetch_oembed_service.rb | 2 +- app/services/fetch_resource_service.rb | 5 +- app/services/follow_migration_service.rb | 30 +- app/services/import_service.rb | 17 +- app/services/notify_service.rb | 15 +- app/services/post_status_service.rb | 29 +- app/services/process_mentions_service.rb | 2 +- app/services/react_service.rb | 4 - app/services/remove_status_service.rb | 2 +- app/services/resolve_account_service.rb | 4 +- app/services/search_service.rb | 14 +- app/services/suspend_account_service.rb | 2 +- app/services/tag_search_service.rb | 2 +- app/services/translate_status_service.rb | 83 +- app/services/unfollow_service.rb | 2 +- app/services/unreact_service.rb | 2 +- app/services/unsuspend_account_service.rb | 2 +- app/services/update_status_service.rb | 4 +- app/services/vote_service.rb | 2 +- app/validators/email_mx_validator.rb | 4 +- app/validators/existing_username_validator.rb | 4 +- app/validators/import_validator.rb | 46 + app/validators/status_pin_validator.rb | 2 +- app/validators/status_reaction_validator.rb | 6 +- app/validators/vote_validator.rb | 26 +- .../_email_domain_block.html.haml | 2 +- .../_domain_block.html.haml | 6 +- app/views/admin/instances/_instance.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 +- app/views/admin/roles/_role.html.haml | 2 +- .../settings/content_retention/show.html.haml | 2 +- .../settings/registrations/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 +- app/views/admin/webhooks/_form.html.haml | 3 - app/views/admin/webhooks/_webhook.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 | 6 +- .../auth/confirmations/captcha.html.haml | 14 +- app/views/auth/registrations/new.html.haml | 11 +- app/views/auth/registrations/rules.html.haml | 14 +- app/views/auth/setup/show.html.haml | 26 +- app/views/auth/shared/_links.html.haml | 2 +- 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 | 6 +- app/views/media/player.html.haml | 6 +- .../notification_mailer/_status.html.haml | 2 +- .../notification_mailer/favourite.html.haml | 2 +- .../notification_mailer/mention.html.haml | 2 +- .../notification_mailer/reblog.html.haml | 2 +- .../authorized_applications/index.html.haml | 6 +- app/views/settings/flavours/show.html.haml | 4 +- app/views/settings/imports/show.html.haml | 20 +- .../_login_activity.html.haml | 2 +- .../preferences/appearance/show.html.haml | 80 +- .../preferences/notifications/show.html.haml | 38 +- .../settings/preferences/other/show.html.haml | 41 +- app/views/statuses/_detailed_status.html.haml | 3 + app/views/statuses/_quote_status.html.haml | 35 + app/views/statuses/_simple_status.html.haml | 4 + .../user_mailer/appeal_approved.html.haml | 2 +- .../user_mailer/appeal_approved.text.erb | 2 +- .../user_mailer/appeal_rejected.html.haml | 2 +- .../user_mailer/appeal_rejected.text.erb | 2 +- .../user_mailer/suspicious_sign_in.html.haml | 2 +- .../user_mailer/suspicious_sign_in.text.erb | 2 +- app/views/user_mailer/warning.html.haml | 2 +- app/workers/distribution_worker.rb | 2 +- app/workers/import/relationship_worker.rb | 3 - 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 | 2 +- .../accounts_statuses_cleanup_scheduler.rb | 100 +- app/workers/scheduler/indexing_scheduler.rb | 14 +- app/workers/scheduler/vacuum_scheduler.rb | 5 - app/workers/webhooks/delivery_worker.rb | 4 +- babel.config.js | 11 +- bin/tootctl | 4 +- config/application.rb | 3 +- config/deploy.rb | 9 +- config/environments/development.rb | 2 +- config/environments/production.rb | 3 +- config/environments/test.rb | 2 +- config/i18n-tasks.yml | 3 - config/initializers/0_duplicate_migrations.rb | 24 +- config/initializers/chewy.rb | 1 - .../initializers/content_security_policy.rb | 8 +- config/initializers/ffmpeg.rb | 2 +- config/initializers/inflections.rb | 1 - config/initializers/omniauth.rb | 2 +- config/initializers/paperclip.rb | 23 +- config/initializers/rack_attack.rb | 8 +- 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 | 9 +- config/locales-glitch/en.yml | 7 +- config/locales-glitch/es-AR.yml | 22 - config/locales-glitch/es-MX.yml | 22 - config/locales-glitch/es.yml | 16 +- config/locales-glitch/fr.yml | 5 + config/locales-glitch/pl.yml | 36 - config/locales-glitch/simple_form.de.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.pl.yml | 17 - config/locales-glitch/simple_form.zh-CN.yml | 15 +- config/locales-glitch/zh-CN.yml | 24 +- config/locales/activerecord.cs.yml | 2 +- config/locales/activerecord.csb.yml | 1 + config/locales/activerecord.fi.yml | 4 +- config/locales/activerecord.hu.yml | 2 +- config/locales/af.yml | 6 + config/locales/an.yml | 9 + config/locales/ar.yml | 25 +- config/locales/ast.yml | 9 + config/locales/be.yml | 32 +- config/locales/bg.yml | 31 +- config/locales/bn.yml | 10 + config/locales/br.yml | 11 + config/locales/bs.yml | 8 + config/locales/ca.yml | 35 +- config/locales/ckb.yml | 9 + config/locales/co.yml | 9 + config/locales/cs.yml | 54 +- config/locales/csb.yml | 10 + config/locales/cy.yml | 31 +- config/locales/da.yml | 31 +- config/locales/de.yml | 73 +- config/locales/devise.bg.yml | 4 +- config/locales/devise.csb.yml | 1 + config/locales/devise.de.yml | 6 +- config/locales/devise.en.yml | 6 +- config/locales/devise.fa.yml | 36 +- config/locales/devise.fi.yml | 38 +- config/locales/devise.hu.yml | 98 +- config/locales/devise.my.yml | 2 - config/locales/devise.nl.yml | 2 +- config/locales/devise.th.yml | 14 +- config/locales/devise.zh-CN.yml | 4 +- config/locales/devise.zh-TW.yml | 6 +- config/locales/doorkeeper.csb.yml | 1 + config/locales/doorkeeper.de.yml | 6 +- config/locales/doorkeeper.es.yml | 2 +- config/locales/doorkeeper.fi.yml | 22 +- config/locales/doorkeeper.gl.yml | 6 +- config/locales/doorkeeper.hu.yml | 70 +- config/locales/doorkeeper.ko.yml | 30 +- config/locales/doorkeeper.ku.yml | 12 - config/locales/doorkeeper.my.yml | 2 - config/locales/doorkeeper.th.yml | 4 +- config/locales/el.yml | 937 ++-- config/locales/en-GB.yml | 868 +--- config/locales/en.yml | 131 +- config/locales/eo.yml | 8 + config/locales/es-AR.yml | 31 +- config/locales/es-MX.yml | 31 +- config/locales/es.yml | 31 +- config/locales/et.yml | 33 +- config/locales/eu.yml | 31 +- config/locales/fa.yml | 209 +- config/locales/fi.yml | 59 +- config/locales/fo.yml | 33 +- config/locales/fr-QC.yml | 15 +- config/locales/fr.yml | 15 +- config/locales/fy.yml | 31 +- config/locales/ga.yml | 12 +- config/locales/gd.yml | 45 +- config/locales/gl.yml | 35 +- config/locales/he.yml | 31 +- config/locales/hi.yml | 17 +- config/locales/hr.yml | 11 + config/locales/hu.yml | 31 +- config/locales/hy.yml | 11 + config/locales/id.yml | 9 + config/locales/ig.yml | 8 + config/locales/io.yml | 9 + config/locales/is.yml | 31 +- config/locales/it.yml | 31 +- config/locales/ja.yml | 29 +- config/locales/ka.yml | 7 + config/locales/kab.yml | 4 + config/locales/kk.yml | 9 + config/locales/kn.yml | 8 + config/locales/ko.yml | 59 +- config/locales/ku.yml | 9 + config/locales/kw.yml | 8 + config/locales/lt.yml | 8 + config/locales/lv.yml | 31 +- config/locales/mk.yml | 8 + config/locales/ml.yml | 11 + config/locales/mr.yml | 8 + config/locales/ms.yml | 2 + config/locales/my.yml | 494 +- config/locales/nl.yml | 37 +- config/locales/nn.yml | 9 + config/locales/no.yml | 42 +- config/locales/oc.yml | 9 + config/locales/pl.yml | 55 +- config/locales/pt-BR.yml | 42 +- config/locales/pt-PT.yml | 31 +- config/locales/ro.yml | 12 + config/locales/ru.yml | 50 +- config/locales/sa.yml | 8 + config/locales/sc.yml | 9 + config/locales/sco.yml | 9 + config/locales/si.yml | 9 + config/locales/simple_form.an.yml | 1 + config/locales/simple_form.ar.yml | 1 + config/locales/simple_form.ast.yml | 1 + config/locales/simple_form.be.yml | 2 +- config/locales/simple_form.bg.yml | 28 +- config/locales/simple_form.ca.yml | 2 +- config/locales/simple_form.ckb.yml | 1 + config/locales/simple_form.co.yml | 1 + config/locales/simple_form.cs.yml | 2 +- config/locales/simple_form.csb.yml | 1 + config/locales/simple_form.cy.yml | 2 +- config/locales/simple_form.da.yml | 2 +- config/locales/simple_form.de.yml | 4 +- config/locales/simple_form.el.yml | 110 +- config/locales/simple_form.en-GB.yml | 2 +- config/locales/simple_form.en.yml | 9 +- config/locales/simple_form.eo.yml | 1 + config/locales/simple_form.es-AR.yml | 1 + config/locales/simple_form.es-MX.yml | 2 +- config/locales/simple_form.es.yml | 2 +- config/locales/simple_form.et.yml | 2 +- config/locales/simple_form.eu.yml | 2 +- config/locales/simple_form.fa.yml | 42 +- config/locales/simple_form.fi.yml | 8 +- config/locales/simple_form.fo.yml | 2 +- config/locales/simple_form.fr-QC.yml | 1 + config/locales/simple_form.fr.yml | 1 + config/locales/simple_form.fy.yml | 2 +- config/locales/simple_form.ga.yml | 12 +- config/locales/simple_form.gd.yml | 1 + config/locales/simple_form.gl.yml | 2 +- config/locales/simple_form.he.yml | 2 +- config/locales/simple_form.hu.yml | 2 +- config/locales/simple_form.hy.yml | 1 + config/locales/simple_form.id.yml | 1 + config/locales/simple_form.io.yml | 1 + config/locales/simple_form.is.yml | 2 +- config/locales/simple_form.it.yml | 2 +- config/locales/simple_form.ja.yml | 2 +- config/locales/simple_form.kab.yml | 1 + config/locales/simple_form.ko.yml | 10 +- config/locales/simple_form.ku.yml | 1 + config/locales/simple_form.lv.yml | 2 +- config/locales/simple_form.my.yml | 61 +- config/locales/simple_form.nl.yml | 6 +- config/locales/simple_form.nn.yml | 1 + config/locales/simple_form.no.yml | 2 +- config/locales/simple_form.oc.yml | 1 + config/locales/simple_form.pl.yml | 4 +- config/locales/simple_form.pt-BR.yml | 3 +- config/locales/simple_form.pt-PT.yml | 2 +- config/locales/simple_form.ro.yml | 1 + config/locales/simple_form.ru.yml | 4 +- config/locales/simple_form.sc.yml | 1 + config/locales/simple_form.sco.yml | 1 + config/locales/simple_form.si.yml | 1 + config/locales/simple_form.sk.yml | 1 + config/locales/simple_form.sl.yml | 2 +- config/locales/simple_form.sq.yml | 2 +- config/locales/simple_form.sr-Latn.yml | 1 + config/locales/simple_form.sr.yml | 2 +- config/locales/simple_form.sv.yml | 2 +- config/locales/simple_form.th.yml | 14 +- config/locales/simple_form.tr.yml | 2 +- config/locales/simple_form.uk.yml | 1 + config/locales/simple_form.vi.yml | 2 +- config/locales/simple_form.zh-CN.yml | 6 +- config/locales/simple_form.zh-HK.yml | 1 + config/locales/simple_form.zh-TW.yml | 8 +- config/locales/sk.yml | 21 +- config/locales/sl.yml | 31 +- config/locales/sq.yml | 30 +- config/locales/sr-Latn.yml | 16 +- config/locales/sr.yml | 31 +- config/locales/sv.yml | 24 +- config/locales/szl.yml | 8 + config/locales/ta.yml | 8 + config/locales/tai.yml | 10 + config/locales/te.yml | 8 + config/locales/th.yml | 49 +- config/locales/tr.yml | 31 +- config/locales/tt.yml | 8 + config/locales/ug.yml | 8 + config/locales/uk.yml | 29 +- config/locales/ur.yml | 8 + config/locales/uz.yml | 8 + config/locales/vi.yml | 27 +- config/locales/zgh.yml | 8 + config/locales/zh-CN.yml | 43 +- config/locales/zh-HK.yml | 22 +- config/locales/zh-TW.yml | 59 +- config/navigation.rb | 2 +- config/routes.rb | 569 +- config/settings.yml | 42 +- 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 | 17 +- config/webpack/tests.js | 1 - config/webpack/translationRunner.js | 122 + config/webpacker.yml | 2 - crowdin.yml | 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 +- ...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 | 2 +- ...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 | 2 +- ...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 | 4 +- ...for_api_v1_accounts_account_id_statuses.rb | 2 +- ...0180528141303_fix_accounts_unique_index.rb | 25 +- ...apply_to_mentions_flag_to_keyword_mutes.rb | 2 - ...08213548_reject_following_blocked_users.rb | 4 +- db/migrate/20180707193142_migrate_filters.rb | 12 +- db/migrate/20180831171112_create_bookmarks.rb | 5 +- ...024224956_migrate_account_conversations.rb | 18 +- ...0512200918_add_content_type_to_statuses.rb | 4 +- ...205_change_list_account_follow_nullable.rb | 2 +- ...00407202420_migrate_unavailable_inboxes.rb | 4 +- ...5_media_attachment_ids_to_timestamp_ids.rb | 2 +- .../20200917192924_add_notify_to_follows.rb | 2 +- ...0306164523_account_ids_to_timestamp_ids.rb | 2 +- ...175231_add_content_type_to_status_edits.rb | 4 +- .../20221124114030_create_status_reactions.rb | 2 - ...20221224204906_add_quote_id_to_statuses.rb | 5 + ...24220348_add_index_to_statuses_quote_id.rb | 7 + .../20180813160548_post_migrate_filters.rb | 6 +- ...emove_suspended_silenced_account_fields.rb | 1 - ...9130537_remove_boosts_widening_audience.rb | 2 +- ...e_subscription_expires_at_from_accounts.rb | 4 +- db/schema.rb | 46 +- db/seeds.rb | 6 +- dist/mastodon-streaming.service | 2 +- dist/nginx.conf | 2 +- jest.config.js | 4 +- lib/cli.rb | 160 + .../post_deployment_migration_generator.rb | 17 + lib/mastodon/accounts_cli.rb | 682 +++ lib/mastodon/cache_cli.rb | 60 + lib/mastodon/canonical_email_blocks_cli.rb | 51 + lib/mastodon/cli_helper.rb | 83 + lib/mastodon/domains_cli.rb | 225 + lib/mastodon/email_domain_blocks_cli.rb | 131 + lib/mastodon/emoji_cli.rb | 147 + lib/mastodon/feeds_cli.rb | 60 + lib/mastodon/ip_blocks_cli.rb | 130 + lib/mastodon/maintenance_cli.rb | 673 +++ lib/mastodon/media_cli.rb | 381 ++ lib/mastodon/premailer_webpack_strategy.rb | 2 +- lib/mastodon/preview_cards_cli.rb | 59 + lib/mastodon/redis_config.rb | 2 +- lib/mastodon/search_cli.rb | 106 + lib/mastodon/settings_cli.rb | 44 + lib/mastodon/snowflake.rb | 2 +- lib/mastodon/statuses_cli.rb | 226 + lib/mastodon/upgrade_cli.rb | 161 + lib/mastodon/version.rb | 10 +- lib/paperclip/color_extractor.rb | 4 +- lib/sanitize_ext/sanitize_config.rb | 11 +- lib/tasks/assets.rake | 12 +- lib/tasks/emojis.rake | 2 +- lib/tasks/glitchsoc.rake | 8 +- lib/tasks/mastodon.rake | 8 +- lib/tasks/tests.rake | 21 +- .../post_deployment_migration/migration.rb | 8 + lib/terrapin/multi_pipe_extensions.rb | 2 +- package.json | 185 +- public/avatars/original/missing.png | Bin 2897 -> 2433 bytes public/favicon.ico | Bin 15406 -> 15086 bytes scalingo.json | 2 +- spec/config/initializers/rack_attack_spec.rb | 34 +- spec/controllers/about_controller_spec.rb | 2 +- spec/controllers/accounts_controller_spec.rb | 26 +- .../collections_controller_spec.rb | 9 +- ...lowers_synchronizations_controller_spec.rb | 9 +- .../activitypub/inboxes_controller_spec.rb | 4 +- .../activitypub/outboxes_controller_spec.rb | 7 +- .../activitypub/replies_controller_spec.rb | 2 +- .../admin/account_actions_controller_spec.rb | 12 - ...ccount_moderation_notes_controller_spec.rb | 8 +- .../admin/accounts_controller_spec.rb | 126 +- .../admin/action_logs_controller_spec.rb | 2 +- .../admin/announcements_controller_spec.rb | 81 - .../controllers/admin/base_controller_spec.rb | 10 +- .../admin/change_emails_controller_spec.rb | 2 +- .../admin/confirmations_controller_spec.rb | 4 +- .../admin/custom_emojis_controller_spec.rb | 2 +- .../admin/dashboard_controller_spec.rb | 2 +- .../admin/disputes/appeals_controller_spec.rb | 12 +- .../admin/domain_allows_controller_spec.rb | 2 +- .../admin/domain_blocks_controller_spec.rb | 141 +- .../email_domain_blocks_controller_spec.rb | 2 +- .../export_domain_allows_controller_spec.rb | 2 +- .../export_domain_blocks_controller_spec.rb | 6 +- .../admin/instances_controller_spec.rb | 2 +- .../admin/invites_controller_spec.rb | 2 +- .../admin/ip_blocks_controller_spec.rb | 33 - .../admin/relays_controller_spec.rb | 79 - .../admin/report_notes_controller_spec.rb | 10 +- .../admin/reports/actions_controller_spec.rb | 6 +- .../admin/rules_controller_spec.rb | 64 - .../settings/branding_controller_spec.rb | 2 +- .../admin/statuses_controller_spec.rb | 16 +- .../controllers/admin/tags_controller_spec.rb | 2 +- .../admin/users/roles_controller_spec.rb | 2 +- .../admin/warning_presets_controller_spec.rb | 64 - .../admin/webhooks_controller_spec.rb | 78 - spec/controllers/api/base_controller_spec.rb | 26 +- .../controllers/api/oembed_controller_spec.rb | 6 +- .../accounts/credentials_controller_spec.rb | 5 +- .../follower_accounts_controller_spec.rb | 4 +- .../following_accounts_controller_spec.rb | 4 +- .../api/v1/accounts/notes_controller_spec.rb | 2 +- .../api/v1/accounts/pins_controller_spec.rb | 2 +- .../accounts/relationships_controller_spec.rb | 45 +- .../api/v1/accounts/search_controller_spec.rb | 2 +- .../v1/accounts/statuses_controller_spec.rb | 2 +- .../api/v1/accounts_controller_spec.rb | 24 +- .../admin/account_actions_controller_spec.rb | 4 +- .../api/v1/admin/accounts_controller_spec.rb | 2 +- .../canonical_email_blocks_controller_spec.rb | 23 + .../v1/admin/domain_allows_controller_spec.rb | 132 + .../v1/admin/domain_blocks_controller_spec.rb | 180 + .../email_domain_blocks_controller_spec.rb | 23 + .../api/v1/admin/ip_blocks_controller_spec.rb | 23 + .../api/v1/admin/reports_controller_spec.rb | 111 + .../v1/admin/trends/links_controller_spec.rb | 49 +- .../admin/trends/statuses_controller_spec.rb | 49 +- .../v1/admin/trends/tags_controller_spec.rb | 49 +- .../reactions_controller_spec.rb | 2 +- .../api/v1/announcements_controller_spec.rb | 2 +- .../api/v1/apps_controller_spec.rb | 2 +- .../api/v1/blocks_controller_spec.rb | 10 +- .../api/v1/bookmarks_controller_spec.rb | 4 +- .../api/v1/conversations_controller_spec.rb | 20 +- .../api/v1/custom_emojis_controller_spec.rb | 2 +- .../api/v1/domain_blocks_controller_spec.rb | 2 +- .../emails/confirmations_controller_spec.rb | 76 +- .../api/v1/endorsements_controller_spec.rb | 2 +- .../api/v1/favourites_controller_spec.rb | 4 +- .../api/v1/featured_tags_controller_spec.rb | 23 + .../api/v1/filters_controller_spec.rb | 2 +- .../api/v1/follow_requests_controller_spec.rb | 2 +- .../api/v1/followed_tags_controller_spec.rb | 2 +- .../v1/instances/activity_controller_spec.rb | 4 +- .../api/v1/instances/peers_controller_spec.rb | 4 +- .../api/v1/instances_controller_spec.rb | 2 +- .../api/v1/lists/accounts_controller_spec.rb | 45 +- .../api/v1/lists_controller_spec.rb | 2 +- .../api/v1/markers_controller_spec.rb | 2 +- .../api/v1/media_controller_spec.rb | 16 +- .../api/v1/mutes_controller_spec.rb | 10 +- .../api/v1/notifications_controller_spec.rb | 2 +- .../api/v1/polls/votes_controller_spec.rb | 2 +- .../api/v1/polls_controller_spec.rb | 2 +- .../api/v1/reports_controller_spec.rb | 4 +- .../favourited_by_accounts_controller_spec.rb | 4 +- .../reblogged_by_accounts_controller_spec.rb | 4 +- .../statuses/translations_controller_spec.rb | 2 +- .../api/v1/statuses_controller_spec.rb | 4 +- .../api/v1/suggestions_controller_spec.rb | 2 +- .../api/v1/tags_controller_spec.rb | 2 +- .../v1/timelines/direct_controller_spec.rb | 2 +- .../api/v1/trends/tags_controller_spec.rb | 2 +- .../api/v2/admin/accounts_controller_spec.rb | 2 +- .../v2/filters/keywords_controller_spec.rb | 2 +- .../v2/filters/statuses_controller_spec.rb | 2 +- .../api/v2/filters_controller_spec.rb | 6 +- .../api/v2/search_controller_spec.rb | 2 +- .../api/web/embeds_controller_spec.rb | 4 +- .../application_controller_spec.rb | 50 +- .../auth/challenges_controller_spec.rb | 2 +- .../auth/confirmations_controller_spec.rb | 2 +- .../auth/passwords_controller_spec.rb | 2 +- .../auth/registrations_controller_spec.rb | 76 +- .../auth/sessions_controller_spec.rb | 48 +- .../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 | 2 +- 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 | 18 +- .../disputes/appeals_controller_spec.rb | 2 +- .../disputes/strikes_controller_spec.rb | 2 +- spec/controllers/emojis_controller_spec.rb | 4 +- .../filters/statuses_controller_spec.rb | 18 +- spec/controllers/filters_controller_spec.rb | 17 +- .../follower_accounts_controller_spec.rb | 4 +- .../following_accounts_controller_spec.rb | 4 +- spec/controllers/home_controller_spec.rb | 2 +- .../instance_actors_controller_spec.rb | 4 +- spec/controllers/intents_controller_spec.rb | 2 +- spec/controllers/invites_controller_spec.rb | 47 +- spec/controllers/manifests_controller_spec.rb | 15 +- .../oauth/authorizations_controller_spec.rb | 7 +- ...authorized_applications_controller_spec.rb | 5 - .../oauth/tokens_controller_spec.rb | 2 +- .../relationships_controller_spec.rb | 76 +- .../settings/aliases_controller_spec.rb | 55 +- .../settings/applications_controller_spec.rb | 28 +- .../settings/deletes_controller_spec.rb | 11 +- .../settings/exports_controller_spec.rb | 12 +- .../settings/featured_tags_controller_spec.rb | 39 +- .../settings/flavours_controller_spec.rb | 3 +- .../settings/imports_controller_spec.rb | 319 +- .../login_activities_controller_spec.rb | 9 +- .../migration/redirects_controller_spec.rb | 54 +- .../settings/pictures_controller_spec.rb | 30 - .../preferences/appearance_controller_spec.rb | 23 +- .../notifications_controller_spec.rb | 23 +- .../preferences/other_controller_spec.rb | 23 +- .../settings/profiles_controller_spec.rb | 19 +- .../confirmations_controller_spec.rb | 31 +- .../webauthn_credentials_controller_spec.rb | 8 +- ..._authentication_methods_controller_spec.rb | 101 +- spec/controllers/shares_controller_spec.rb | 2 +- .../statuses_cleanup_controller_spec.rb | 21 +- spec/controllers/statuses_controller_spec.rb | 255 +- spec/controllers/tags_controller_spec.rb | 47 +- .../well_known/host_meta_controller_spec.rb | 2 +- .../well_known/nodeinfo_controller_spec.rb | 2 +- .../well_known/webfinger_controller_spec.rb | 46 +- .../account_domain_block_fabricator.rb | 2 +- .../account_moderation_note_fabricator.rb | 4 +- spec/fabricators/account_note_fabricator.rb | 4 +- spec/fabricators/account_stat_fabricator.rb | 2 +- ...ount_statuses_cleanup_policy_fabricator.rb | 2 +- .../fabricators/account_warning_fabricator.rb | 2 +- .../admin_action_log_fabricator.rb | 2 +- spec/fabricators/backup_fabricator.rb | 2 +- spec/fabricators/block_fabricator.rb | 4 +- spec/fabricators/bookmark_fabricator.rb | 4 +- .../canonical_email_block_fabricator.rb | 4 +- spec/fabricators/custom_filter_fabricator.rb | 2 +- .../custom_filter_keyword_fabricator.rb | 2 +- .../custom_filter_status_fabricator.rb | 4 +- spec/fabricators/device_fabricator.rb | 4 +- spec/fabricators/domain_allow_fabricator.rb | 2 +- .../encrypted_message_fabricator.rb | 4 +- spec/fabricators/favourite_fabricator.rb | 4 +- spec/fabricators/follow_fabricator.rb | 4 +- spec/fabricators/follow_request_fabricator.rb | 4 +- spec/fabricators/identity_fabricator.rb | 2 +- spec/fabricators/invite_fabricator.rb | 2 +- spec/fabricators/list_fabricator.rb | 2 +- spec/fabricators/login_activity_fabricator.rb | 2 +- spec/fabricators/marker_fabricator.rb | 2 +- .../media_attachment_fabricator.rb | 2 +- spec/fabricators/mention_fabricator.rb | 4 +- spec/fabricators/mute_fabricator.rb | 4 +- spec/fabricators/notification_fabricator.rb | 4 +- spec/fabricators/one_time_key_fabricator.rb | 2 +- spec/fabricators/poll_fabricator.rb | 4 +- spec/fabricators/poll_vote_fabricator.rb | 2 +- spec/fabricators/report_fabricator.rb | 4 +- spec/fabricators/report_note_fabricator.rb | 4 +- .../scheduled_status_fabricator.rb | 2 +- .../session_activation_fabricator.rb | 2 +- spec/fabricators/status_fabricator.rb | 2 +- spec/fabricators/status_pin_fabricator.rb | 4 +- .../fabricators/status_reaction_fabricator.rb | 12 +- spec/fabricators/tag_follow_fabricator.rb | 2 +- spec/features/log_in_spec.rb | 2 +- spec/helpers/accounts_helper_spec.rb | 2 +- .../account_moderation_notes_helper_spec.rb | 6 +- spec/helpers/admin/action_logs_helper_spec.rb | 2 +- spec/helpers/application_helper_spec.rb | 207 +- spec/helpers/flashes_helper_spec.rb | 2 +- spec/helpers/formatting_helper_spec.rb | 2 +- spec/helpers/home_helper_spec.rb | 2 +- spec/helpers/jsonld_helper_spec.rb | 10 +- spec/helpers/routing_helper_spec.rb | 6 +- spec/lib/activitypub/activity/accept_spec.rb | 2 +- .../lib/activitypub/activity/announce_spec.rb | 12 +- spec/lib/activitypub/activity/create_spec.rb | 38 +- spec/lib/activitypub/activity/flag_spec.rb | 31 - spec/lib/activitypub/activity/follow_spec.rb | 20 +- spec/lib/activitypub/activity/reject_spec.rb | 14 +- spec/lib/activitypub/activity/undo_spec.rb | 2 +- spec/lib/activitypub/adapter_spec.rb | 76 +- 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 | 10 +- spec/lib/entity_cache_spec.rb | 4 +- spec/lib/extractor_spec.rb | 20 +- spec/lib/feed_manager_spec.rb | 195 +- spec/lib/hash_object_spec.rb | 9 + spec/lib/html_aware_formatter_spec.rb | 6 +- spec/lib/link_details_extractor_spec.rb | 4 +- spec/lib/mastodon/cli_spec.rb | 14 + spec/lib/ostatus/tag_manager_spec.rb | 22 +- spec/lib/request_pool_spec.rb | 2 +- spec/lib/request_spec.rb | 2 +- spec/lib/scope_transformer_spec.rb | 22 +- 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 | 16 +- spec/lib/status_reach_finder_spec.rb | 14 +- spec/lib/tag_manager_spec.rb | 22 +- spec/lib/text_formatter_spec.rb | 66 +- spec/lib/translation_service/deepl_spec.rb | 26 +- .../libre_translate_spec.rb | 34 +- spec/lib/user_settings_decorator_spec.rb | 84 + spec/lib/vacuum/access_tokens_vacuum_spec.rb | 10 - spec/lib/webfinger_resource_spec.rb | 28 +- spec/mailers/admin_mailer_spec.rb | 2 +- spec/mailers/notification_mailer_spec.rb | 28 +- spec/mailers/user_mailer_spec.rb | 24 +- spec/models/account/field_spec.rb | 32 +- spec/models/account_alias_spec.rb | 2 +- spec/models/account_conversation_spec.rb | 18 +- spec/models/account_deletion_request_spec.rb | 2 +- spec/models/account_domain_block_spec.rb | 6 +- spec/models/account_filter_spec.rb | 27 +- spec/models/account_migration_spec.rb | 6 +- spec/models/account_moderation_note_spec.rb | 2 +- spec/models/account_spec.rb | 116 +- .../account_statuses_cleanup_policy_spec.rb | 4 +- spec/models/admin/account_action_spec.rb | 38 +- spec/models/admin/action_log_spec.rb | 2 +- spec/models/announcement_mute_spec.rb | 2 +- spec/models/announcement_reaction_spec.rb | 2 +- spec/models/announcement_spec.rb | 2 +- spec/models/backup_spec.rb | 2 +- spec/models/block_spec.rb | 6 +- spec/models/canonical_email_block_spec.rb | 2 +- .../concerns/account_interactions_spec.rb | 169 +- spec/models/concerns/remotable_spec.rb | 51 +- spec/models/conversation_mute_spec.rb | 2 +- spec/models/conversation_spec.rb | 2 +- spec/models/custom_emoji_filter_spec.rb | 24 +- spec/models/custom_emoji_spec.rb | 10 +- spec/models/custom_filter_keyword_spec.rb | 2 +- spec/models/custom_filter_spec.rb | 2 +- spec/models/device_spec.rb | 2 +- spec/models/domain_block_spec.rb | 56 +- spec/models/email_domain_block_spec.rb | 16 +- spec/models/encrypted_message_spec.rb | 2 +- spec/models/export_spec.rb | 26 +- spec/models/favourite_spec.rb | 10 +- spec/models/featured_tag_spec.rb | 2 +- .../follow_recommendation_suppression_spec.rb | 2 +- spec/models/follow_request_spec.rb | 44 +- spec/models/follow_spec.rb | 10 +- spec/models/home_feed_spec.rb | 2 +- spec/models/identity_spec.rb | 4 +- spec/models/import_spec.rb | 23 +- spec/models/invite_spec.rb | 2 +- spec/models/list_account_spec.rb | 2 +- spec/models/list_spec.rb | 2 +- spec/models/login_activity_spec.rb | 2 +- spec/models/media_attachment_spec.rb | 36 +- spec/models/mention_spec.rb | 2 +- spec/models/mute_spec.rb | 2 +- spec/models/notification_spec.rb | 140 +- spec/models/poll_vote_spec.rb | 2 +- spec/models/preview_card_spec.rb | 2 +- spec/models/preview_card_trend_spec.rb | 2 +- spec/models/public_feed_spec.rb | 14 +- spec/models/relationship_filter_spec.rb | 2 +- spec/models/relay_spec.rb | 2 +- spec/models/remote_follow_spec.rb | 8 +- spec/models/report_filter_spec.rb | 6 +- spec/models/report_spec.rb | 15 +- spec/models/scheduled_status_spec.rb | 2 +- spec/models/session_activation_spec.rb | 16 +- spec/models/setting_spec.rb | 32 +- spec/models/site_upload_spec.rb | 4 +- spec/models/status_pin_spec.rb | 20 +- spec/models/status_reaction_spec.rb | 6 +- spec/models/status_spec.rb | 122 +- spec/models/status_stat_spec.rb | 2 +- spec/models/status_trend_spec.rb | 2 +- spec/models/system_key_spec.rb | 2 +- spec/models/tag_feed_spec.rb | 2 +- spec/models/tag_follow_spec.rb | 2 +- spec/models/trends/statuses_spec.rb | 2 +- spec/models/trends/tags_spec.rb | 2 +- spec/models/unavailable_domain_spec.rb | 2 +- spec/models/user_invite_request_spec.rb | 2 +- spec/models/user_role_spec.rb | 8 +- spec/models/user_spec.rb | 80 +- spec/models/web/push_subscription_spec.rb | 10 +- spec/models/web/setting_spec.rb | 2 +- spec/models/webauthn_credentials_spec.rb | 2 +- spec/models/webhook_spec.rb | 2 +- .../account_moderation_note_policy_spec.rb | 14 +- spec/policies/account_policy_spec.rb | 40 +- spec/policies/backup_policy_spec.rb | 10 +- spec/policies/custom_emoji_policy_spec.rb | 8 +- spec/policies/domain_block_policy_spec.rb | 4 +- .../email_domain_block_policy_spec.rb | 4 +- spec/policies/instance_policy_spec.rb | 4 +- spec/policies/invite_policy_spec.rb | 18 +- spec/policies/relay_policy_spec.rb | 4 +- spec/policies/report_note_policy_spec.rb | 26 +- spec/policies/report_policy_spec.rb | 4 +- spec/policies/settings_policy_spec.rb | 4 +- spec/policies/status_policy_spec.rb | 192 +- spec/policies/tag_policy_spec.rb | 4 +- spec/policies/user_policy_spec.rb | 36 +- .../account_relationships_presenter_spec.rb | 18 +- .../familiar_followers_presenter_spec.rb | 2 +- .../status_relationships_presenter_spec.rb | 16 +- spec/rails_helper.rb | 58 +- spec/requests/link_headers_spec.rb | 4 +- spec/requests/localization_spec.rb | 6 +- .../activitypub/note_serializer_spec.rb | 2 +- .../update_poll_serializer_spec.rb | 2 +- .../rest/account_serializer_spec.rb | 2 +- spec/services/account_search_service_spec.rb | 2 +- .../account_statuses_cleanup_service_spec.rb | 4 +- .../fetch_featured_collection_service_spec.rb | 2 +- .../fetch_remote_account_service_spec.rb | 2 +- .../fetch_remote_actor_service_spec.rb | 2 +- .../fetch_remote_key_service_spec.rb | 2 +- .../fetch_remote_status_service_spec.rb | 6 +- .../process_account_service_spec.rb | 18 +- .../process_status_update_service_spec.rb | 16 +- ..._block_domain_from_account_service_spec.rb | 2 +- .../services/authorize_follow_service_spec.rb | 2 +- .../batched_remove_status_service_spec.rb | 2 +- spec/services/block_domain_service_spec.rb | 2 +- spec/services/block_service_spec.rb | 2 +- .../bootstrap_timeline_service_spec.rb | 2 +- .../clear_domain_media_service_spec.rb | 2 +- spec/services/favourite_service_spec.rb | 2 +- spec/services/fetch_link_card_service_spec.rb | 30 +- spec/services/fetch_oembed_service_spec.rb | 12 +- .../fetch_remote_status_service_spec.rb | 2 +- spec/services/follow_service_spec.rb | 6 +- spec/services/import_service_spec.rb | 24 +- spec/services/notify_service_spec.rb | 25 +- spec/services/post_status_service_spec.rb | 4 +- spec/services/precompute_feed_service_spec.rb | 4 +- .../services/process_mentions_service_spec.rb | 12 +- spec/services/purge_domain_service_spec.rb | 2 +- spec/services/reblog_service_spec.rb | 27 +- spec/services/reject_follow_service_spec.rb | 2 +- .../remove_from_followers_service_spec.rb | 2 +- spec/services/remove_status_service_spec.rb | 2 +- spec/services/report_service_spec.rb | 17 +- spec/services/resolve_account_service_spec.rb | 8 +- spec/services/resolve_url_service_spec.rb | 6 +- spec/services/search_service_spec.rb | 10 +- spec/services/suspend_account_service_spec.rb | 2 +- spec/services/unallow_domain_service_spec.rb | 4 +- spec/services/unblock_service_spec.rb | 2 +- spec/services/unfollow_service_spec.rb | 2 +- spec/services/unmute_service_spec.rb | 2 +- .../unsuspend_account_service_spec.rb | 12 +- spec/services/update_account_service_spec.rb | 2 +- spec/services/update_status_service_spec.rb | 11 +- spec/services/verify_link_service_spec.rb | 4 +- spec/spec_helper.rb | 2 +- .../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 +- .../disallowed_hashtags_validator_spec.rb | 4 +- spec/validators/email_mx_validator_spec.rb | 2 +- .../validators/follow_limit_validator_spec.rb | 8 +- spec/validators/note_length_validator_spec.rb | 2 +- spec/validators/poll_validator_spec.rb | 2 +- .../status_length_validator_spec.rb | 2 +- spec/validators/status_pin_validator_spec.rb | 8 +- .../unreserved_username_validator_spec.rb | 8 +- spec/validators/url_validator_spec.rb | 4 +- spec/views/statuses/show.html.haml_spec.rb | 12 +- spec/workers/move_worker_spec.rb | 113 +- .../poll_expiration_notify_worker_spec.rb | 61 +- ...ccounts_statuses_cleanup_scheduler_spec.rb | 120 +- streaming/index.js | 257 +- stylelint.config.js | 22 +- yarn.lock | 4595 +++++++---------- 2429 files changed, 43232 insertions(+), 32361 deletions(-) create mode 100644 app/controllers/settings/preferences_controller.rb create mode 100644 app/javascript/flavours/glitch/actions/app.js create mode 100644 app/javascript/flavours/glitch/actions/modal.js create mode 100644 app/javascript/flavours/glitch/base_polyfills.js create mode 100644 app/javascript/flavours/glitch/blurhash.js create mode 100644 app/javascript/flavours/glitch/compare_id.js create mode 100644 app/javascript/flavours/glitch/components/animated_number.jsx create mode 100644 app/javascript/flavours/glitch/components/avatar.jsx create mode 100644 app/javascript/flavours/glitch/components/blurhash.jsx create mode 100644 app/javascript/flavours/glitch/components/display_name.jsx create mode 100644 app/javascript/flavours/glitch/components/domain.jsx create mode 100644 app/javascript/flavours/glitch/components/gifv.jsx create mode 100644 app/javascript/flavours/glitch/components/icon.jsx create mode 100644 app/javascript/flavours/glitch/components/icon_button.jsx create mode 100644 app/javascript/flavours/glitch/components/icon_with_badge.jsx create mode 100644 app/javascript/flavours/glitch/components/image.jsx create mode 100644 app/javascript/flavours/glitch/components/load_gap.jsx create mode 100644 app/javascript/flavours/glitch/components/load_more.jsx create mode 100644 app/javascript/flavours/glitch/components/load_pending.jsx create mode 100644 app/javascript/flavours/glitch/components/loading_indicator.jsx create mode 100644 app/javascript/flavours/glitch/components/missing_indicator.jsx create mode 100644 app/javascript/flavours/glitch/components/name_list.js create mode 100644 app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx create mode 100644 app/javascript/flavours/glitch/components/radio_button.jsx create mode 100644 app/javascript/flavours/glitch/components/relative_timestamp.jsx create mode 100644 app/javascript/flavours/glitch/components/skeleton.jsx create mode 100644 app/javascript/flavours/glitch/components/spoilers.jsx create mode 100644 app/javascript/flavours/glitch/components/status_reactions.js create mode 100644 app/javascript/flavours/glitch/components/timeline_hint.jsx create mode 100644 app/javascript/flavours/glitch/extra_polyfills.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/quote_indicator.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/quote_indicator_container.js create mode 100644 app/javascript/flavours/glitch/features/emoji/emoji_mart_data_light.js create mode 100644 app/javascript/flavours/glitch/features/generic_not_found/index.jsx create mode 100644 app/javascript/flavours/glitch/is_mobile.js create mode 100644 app/javascript/flavours/glitch/load_polyfills.js create mode 100644 app/javascript/flavours/glitch/locales/defaultMessages.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_af.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ar.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ast.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_bg.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_bn.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_br.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ca.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ckb.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_co.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_cs.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_cy.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_da.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_de.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_el.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_en.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_eo.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_es-AR.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_es-MX.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_es.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_et.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_eu.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_fa.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_fi.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_fr.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ga.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_gd.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_gl.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_he.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_hi.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_hr.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_hu.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_hy.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_id.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_io.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_is.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_it.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ja.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ka.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_kab.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_kk.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_kn.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ko.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ku.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_kw.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_lt.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_lv.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_mk.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ml.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_mr.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ms.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_nl.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_nn.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_no.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_oc.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_pa.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_pl.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_pt-BR.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_pt-PT.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ro.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ru.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_sa.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_sc.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_si.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_sk.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_sl.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_sq.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_sr-Latn.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_sr.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_sv.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_szl.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ta.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_tai.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_te.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_th.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_tr.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_tt.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ug.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_uk.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_ur.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_vi.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_zgh.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_zh-CN.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_zh-HK.json create mode 100644 app/javascript/flavours/glitch/locales/whitelist_zh-TW.json create mode 100644 app/javascript/flavours/glitch/middleware/errors.js create mode 100644 app/javascript/flavours/glitch/middleware/loading_bar.js create mode 100644 app/javascript/flavours/glitch/middleware/sounds.js create mode 100644 app/javascript/flavours/glitch/permissions.js create mode 100644 app/javascript/flavours/glitch/reducers/index.js create mode 100644 app/javascript/flavours/glitch/reducers/modal.js create mode 100644 app/javascript/flavours/glitch/scroll.js create mode 100644 app/javascript/flavours/glitch/store/configureStore.js create mode 100644 app/javascript/flavours/glitch/styles/bird.scss create mode 100644 app/javascript/flavours/glitch/styles/bird/diff.scss create mode 100644 app/javascript/flavours/glitch/styles/bird/variables.scss create mode 100644 app/javascript/flavours/glitch/utils/base64.js create mode 100644 app/javascript/flavours/glitch/utils/filters.js create mode 100644 app/javascript/flavours/glitch/utils/numbers.js create mode 100644 app/javascript/flavours/glitch/utils/resize_image.js create mode 100644 app/javascript/flavours/glitch/uuid.js create mode 100644 app/javascript/locales/index.js create mode 100644 app/javascript/locales/locale-data/README.md create mode 100644 app/javascript/locales/locale-data/oc.js create mode 100644 app/javascript/mastodon/actions/app.js create mode 100644 app/javascript/mastodon/actions/modal.js create mode 100644 app/javascript/mastodon/base_polyfills.js create mode 100644 app/javascript/mastodon/blurhash.js create mode 100644 app/javascript/mastodon/compare_id.js create mode 100644 app/javascript/mastodon/components/animated_number.jsx create mode 100644 app/javascript/mastodon/components/avatar.jsx create mode 100644 app/javascript/mastodon/components/avatar_overlay.jsx create mode 100644 app/javascript/mastodon/components/blurhash.jsx create mode 100644 app/javascript/mastodon/components/check.jsx create mode 100644 app/javascript/mastodon/components/display_name.jsx create mode 100644 app/javascript/mastodon/components/domain.jsx create mode 100644 app/javascript/mastodon/components/gifv.jsx create mode 100644 app/javascript/mastodon/components/icon.jsx create mode 100644 app/javascript/mastodon/components/icon_button.jsx create mode 100644 app/javascript/mastodon/components/icon_with_badge.jsx create mode 100644 app/javascript/mastodon/components/image.jsx create mode 100644 app/javascript/mastodon/components/load_gap.jsx create mode 100644 app/javascript/mastodon/components/load_more.jsx create mode 100644 app/javascript/mastodon/components/load_pending.jsx create mode 100644 app/javascript/mastodon/components/loading_indicator.jsx create mode 100644 app/javascript/mastodon/components/logo.jsx create mode 100644 app/javascript/mastodon/components/missing_indicator.jsx create mode 100644 app/javascript/mastodon/components/not_signed_in_indicator.jsx create mode 100644 app/javascript/mastodon/components/radio_button.jsx create mode 100644 app/javascript/mastodon/components/relative_timestamp.jsx create mode 100644 app/javascript/mastodon/components/skeleton.jsx create mode 100644 app/javascript/mastodon/components/status_reactions.js create mode 100644 app/javascript/mastodon/components/timeline_hint.jsx create mode 100644 app/javascript/mastodon/extra_polyfills.js create mode 100644 app/javascript/mastodon/features/emoji/emoji_mart_data_light.js create mode 100644 app/javascript/mastodon/features/follow_recommendations/components/account.jsx create mode 100644 app/javascript/mastodon/features/follow_recommendations/index.jsx create mode 100644 app/javascript/mastodon/features/generic_not_found/index.jsx create mode 100644 app/javascript/mastodon/is_mobile.js create mode 100644 app/javascript/mastodon/load_polyfills.js create mode 100644 app/javascript/mastodon/locales/csb.json create mode 100644 app/javascript/mastodon/locales/defaultMessages.json create mode 100644 app/javascript/mastodon/locales/index.js create mode 100644 app/javascript/mastodon/locales/locale-data/co.js create mode 100644 app/javascript/mastodon/locales/locale-data/sa.js create mode 100644 app/javascript/mastodon/locales/whitelist_af.json create mode 100644 app/javascript/mastodon/locales/whitelist_an.json create mode 100644 app/javascript/mastodon/locales/whitelist_ar.json create mode 100644 app/javascript/mastodon/locales/whitelist_ast.json create mode 100644 app/javascript/mastodon/locales/whitelist_be.json create mode 100644 app/javascript/mastodon/locales/whitelist_bg.json create mode 100644 app/javascript/mastodon/locales/whitelist_bn.json create mode 100644 app/javascript/mastodon/locales/whitelist_br.json create mode 100644 app/javascript/mastodon/locales/whitelist_bs.json create mode 100644 app/javascript/mastodon/locales/whitelist_ca.json create mode 100644 app/javascript/mastodon/locales/whitelist_ckb.json create mode 100644 app/javascript/mastodon/locales/whitelist_co.json create mode 100644 app/javascript/mastodon/locales/whitelist_cs.json create mode 100644 app/javascript/mastodon/locales/whitelist_csb.json create mode 100644 app/javascript/mastodon/locales/whitelist_cy.json create mode 100644 app/javascript/mastodon/locales/whitelist_da.json create mode 100644 app/javascript/mastodon/locales/whitelist_de.json create mode 100644 app/javascript/mastodon/locales/whitelist_el.json create mode 100644 app/javascript/mastodon/locales/whitelist_en-GB.json create mode 100644 app/javascript/mastodon/locales/whitelist_en.json create mode 100644 app/javascript/mastodon/locales/whitelist_eo.json create mode 100644 app/javascript/mastodon/locales/whitelist_es-AR.json create mode 100644 app/javascript/mastodon/locales/whitelist_es-MX.json create mode 100644 app/javascript/mastodon/locales/whitelist_es.json create mode 100644 app/javascript/mastodon/locales/whitelist_et.json create mode 100644 app/javascript/mastodon/locales/whitelist_eu.json create mode 100644 app/javascript/mastodon/locales/whitelist_fa.json create mode 100644 app/javascript/mastodon/locales/whitelist_fi.json create mode 100644 app/javascript/mastodon/locales/whitelist_fo.json create mode 100644 app/javascript/mastodon/locales/whitelist_fr-QC.json create mode 100644 app/javascript/mastodon/locales/whitelist_fr.json create mode 100644 app/javascript/mastodon/locales/whitelist_fy.json create mode 100644 app/javascript/mastodon/locales/whitelist_ga.json create mode 100644 app/javascript/mastodon/locales/whitelist_gd.json create mode 100644 app/javascript/mastodon/locales/whitelist_gl.json create mode 100644 app/javascript/mastodon/locales/whitelist_he.json create mode 100644 app/javascript/mastodon/locales/whitelist_hi.json create mode 100644 app/javascript/mastodon/locales/whitelist_hr.json create mode 100644 app/javascript/mastodon/locales/whitelist_hu.json create mode 100644 app/javascript/mastodon/locales/whitelist_hy.json create mode 100644 app/javascript/mastodon/locales/whitelist_id.json create mode 100644 app/javascript/mastodon/locales/whitelist_ig.json create mode 100644 app/javascript/mastodon/locales/whitelist_io.json create mode 100644 app/javascript/mastodon/locales/whitelist_is.json create mode 100644 app/javascript/mastodon/locales/whitelist_it.json create mode 100644 app/javascript/mastodon/locales/whitelist_ja.json create mode 100644 app/javascript/mastodon/locales/whitelist_ka.json create mode 100644 app/javascript/mastodon/locales/whitelist_kab.json create mode 100644 app/javascript/mastodon/locales/whitelist_kk.json create mode 100644 app/javascript/mastodon/locales/whitelist_kn.json create mode 100644 app/javascript/mastodon/locales/whitelist_ko.json create mode 100644 app/javascript/mastodon/locales/whitelist_ku.json create mode 100644 app/javascript/mastodon/locales/whitelist_kw.json create mode 100644 app/javascript/mastodon/locales/whitelist_la.json create mode 100644 app/javascript/mastodon/locales/whitelist_lt.json create mode 100644 app/javascript/mastodon/locales/whitelist_lv.json create mode 100644 app/javascript/mastodon/locales/whitelist_mk.json create mode 100644 app/javascript/mastodon/locales/whitelist_ml.json create mode 100644 app/javascript/mastodon/locales/whitelist_mr.json create mode 100644 app/javascript/mastodon/locales/whitelist_ms.json create mode 100644 app/javascript/mastodon/locales/whitelist_my.json create mode 100644 app/javascript/mastodon/locales/whitelist_nl.json create mode 100644 app/javascript/mastodon/locales/whitelist_nn.json create mode 100644 app/javascript/mastodon/locales/whitelist_no.json create mode 100644 app/javascript/mastodon/locales/whitelist_oc.json create mode 100644 app/javascript/mastodon/locales/whitelist_pa.json create mode 100644 app/javascript/mastodon/locales/whitelist_pl.json create mode 100644 app/javascript/mastodon/locales/whitelist_pt-BR.json create mode 100644 app/javascript/mastodon/locales/whitelist_pt-PT.json create mode 100644 app/javascript/mastodon/locales/whitelist_ro.json create mode 100644 app/javascript/mastodon/locales/whitelist_ru.json create mode 100644 app/javascript/mastodon/locales/whitelist_sa.json create mode 100644 app/javascript/mastodon/locales/whitelist_sc.json create mode 100644 app/javascript/mastodon/locales/whitelist_sco.json create mode 100644 app/javascript/mastodon/locales/whitelist_si.json create mode 100644 app/javascript/mastodon/locales/whitelist_sk.json create mode 100644 app/javascript/mastodon/locales/whitelist_sl.json create mode 100644 app/javascript/mastodon/locales/whitelist_sq.json create mode 100644 app/javascript/mastodon/locales/whitelist_sr-Latn.json create mode 100644 app/javascript/mastodon/locales/whitelist_sr.json create mode 100644 app/javascript/mastodon/locales/whitelist_sv.json create mode 100644 app/javascript/mastodon/locales/whitelist_szl.json create mode 100644 app/javascript/mastodon/locales/whitelist_ta.json create mode 100644 app/javascript/mastodon/locales/whitelist_tai.json create mode 100644 app/javascript/mastodon/locales/whitelist_te.json create mode 100644 app/javascript/mastodon/locales/whitelist_th.json create mode 100644 app/javascript/mastodon/locales/whitelist_tr.json create mode 100644 app/javascript/mastodon/locales/whitelist_tt.json create mode 100644 app/javascript/mastodon/locales/whitelist_ug.json create mode 100644 app/javascript/mastodon/locales/whitelist_uk.json create mode 100644 app/javascript/mastodon/locales/whitelist_ur.json create mode 100644 app/javascript/mastodon/locales/whitelist_uz.json create mode 100644 app/javascript/mastodon/locales/whitelist_vi.json create mode 100644 app/javascript/mastodon/locales/whitelist_zgh.json create mode 100644 app/javascript/mastodon/locales/whitelist_zh-CN.json create mode 100644 app/javascript/mastodon/locales/whitelist_zh-HK.json create mode 100644 app/javascript/mastodon/locales/whitelist_zh-TW.json create mode 100644 app/javascript/mastodon/middleware/errors.js create mode 100644 app/javascript/mastodon/middleware/loading_bar.js create mode 100644 app/javascript/mastodon/middleware/sounds.js create mode 100644 app/javascript/mastodon/permissions.js create mode 100644 app/javascript/mastodon/reducers/index.js create mode 100644 app/javascript/mastodon/reducers/missed_updates.js create mode 100644 app/javascript/mastodon/reducers/modal.js create mode 100644 app/javascript/mastodon/scroll.js create mode 100644 app/javascript/mastodon/store/configureStore.js create mode 100644 app/javascript/mastodon/utils/base64.js create mode 100644 app/javascript/mastodon/utils/filters.js create mode 100644 app/javascript/mastodon/utils/numbers.js create mode 100644 app/javascript/mastodon/utils/resize_image.js create mode 100644 app/javascript/mastodon/uuid.js create mode 100644 app/javascript/skins/glitch/Mastodon-Bird/common.scss create mode 100644 app/javascript/skins/glitch/Mastodon-Bird/names.yml create mode 100644 app/javascript/skins/vanilla/contrast-modern/common.scss create mode 100644 app/javascript/skins/vanilla/contrast-modern/names.yml create mode 100644 app/javascript/skins/vanilla/mastodon-birdsite/common.scss create mode 100644 app/javascript/skins/vanilla/mastodon-birdsite/names.yml create mode 100644 app/javascript/skins/vanilla/mastodon-light-modern/common.scss create mode 100644 app/javascript/skins/vanilla/mastodon-light-modern/names.yml create mode 100644 app/javascript/skins/vanilla/mastodon-modern/common.scss create mode 100644 app/javascript/skins/vanilla/mastodon-modern/names.yml create mode 100644 app/javascript/styles/birdsite.css create mode 100644 app/javascript/styles/contrast-modern.scss create mode 100644 app/javascript/styles/mastodon-birdsite.scss create mode 100644 app/javascript/styles/mastodon-light-modern.scss create mode 100644 app/javascript/styles/mastodon-modern.scss create mode 100644 app/javascript/styles/modern.scss create mode 100644 app/lib/hash_object.rb create mode 100644 app/lib/settings/extend.rb create mode 100644 app/lib/user_settings_decorator.rb create mode 100644 app/validators/import_validator.rb create mode 100644 app/views/statuses/_quote_status.html.haml create mode 100644 config/initializers/statsd.rb create mode 100644 config/locales/activerecord.csb.yml create mode 100644 config/locales/csb.yml create mode 100644 config/locales/devise.csb.yml create mode 100644 config/locales/doorkeeper.csb.yml create mode 100644 config/locales/simple_form.csb.yml create mode 100644 config/locales/tai.yml create mode 100644 config/webpack/generateLocalePacks.js create mode 100644 config/webpack/translationRunner.js create mode 100644 db/migrate/20221224204906_add_quote_id_to_statuses.rb create mode 100644 db/migrate/20221224220348_add_index_to_statuses_quote_id.rb create mode 100644 lib/cli.rb create mode 100644 lib/generators/post_deployment_migration_generator.rb create mode 100644 lib/mastodon/accounts_cli.rb create mode 100644 lib/mastodon/cache_cli.rb create mode 100644 lib/mastodon/canonical_email_blocks_cli.rb create mode 100644 lib/mastodon/cli_helper.rb create mode 100644 lib/mastodon/domains_cli.rb create mode 100644 lib/mastodon/email_domain_blocks_cli.rb create mode 100644 lib/mastodon/emoji_cli.rb create mode 100644 lib/mastodon/feeds_cli.rb create mode 100644 lib/mastodon/ip_blocks_cli.rb create mode 100644 lib/mastodon/maintenance_cli.rb create mode 100644 lib/mastodon/media_cli.rb create mode 100644 lib/mastodon/preview_cards_cli.rb create mode 100644 lib/mastodon/search_cli.rb create mode 100644 lib/mastodon/settings_cli.rb create mode 100644 lib/mastodon/statuses_cli.rb create mode 100644 lib/mastodon/upgrade_cli.rb create mode 100644 lib/templates/rails/post_deployment_migration/migration.rb create mode 100644 spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/domain_allows_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/domain_blocks_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb create mode 100644 spec/controllers/api/v1/admin/reports_controller_spec.rb create mode 100644 spec/controllers/api/v1/featured_tags_controller_spec.rb create mode 100644 spec/lib/hash_object_spec.rb create mode 100644 spec/lib/mastodon/cli_spec.rb create mode 100644 spec/lib/settings/extend_spec.rb create mode 100644 spec/lib/settings/scoped_settings_spec.rb create mode 100644 spec/lib/user_settings_decorator_spec.rb create mode 100644 spec/support/examples/lib/settings/scoped_settings.rb create mode 100644 spec/support/examples/lib/settings/settings_extended.rb diff --git a/Aptfile b/Aptfile index 5e033f136..8f5bb72a2 100644 --- a/Aptfile +++ b/Aptfile @@ -1,5 +1,4 @@ ffmpeg -libopenblas0-pthread libpq-dev libxdamage1 libxfixes3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a2c48a1..2b826fb14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,71 +2,6 @@ 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a232915b6..c7c0a39b8 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,26 +12,26 @@ 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. diff --git a/Dockerfile b/Dockerfile index cb5096581..c2b18ce88 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.20-bullseye-slim" +ARG NODE_VERSION="16.19-bullseye-slim" -FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby +FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.1-slim as ruby FROM node:${NODE_VERSION} as build COPY --link --from=ruby /opt/ruby /opt/ruby @@ -18,6 +18,7 @@ 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 \ @@ -36,15 +37,11 @@ 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 --production --network-timeout 600000 && \ + yarn install --pure-lockfile --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" @@ -55,7 +52,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"] ENV DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" -# Ignoring these here since we don't want to pin any versions and the Debian image removes apt-get content after use +# Ignoreing 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 && \ @@ -88,9 +85,7 @@ 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" \ - MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \ - MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}" + BIND="0.0.0.0" # Set the run user USER mastodon diff --git a/Gemfile b/Gemfile index 84f210f48..752874978 100644 --- a/Gemfile +++ b/Gemfile @@ -1,26 +1,26 @@ # frozen_string_literal: true source 'https://rubygems.org' -ruby '>= 3.0.0' +ruby '>= 2.7.0', '< 3.3.0' gem 'pkg-config', '~> 1.5' -gem 'puma', '~> 6.3' +gem 'puma', '~> 6.1' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' -gem 'rack', '~> 2.2.7' +gem 'rack', '~> 2.2.6' gem 'haml-rails', '~>2.0' -gem 'pg', '~> 1.5' +gem 'pg', '~> 1.4' gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.123', require: false +gem 'aws-sdk-s3', '~> 1.119', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.2' +gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' @@ -28,15 +28,15 @@ gem 'addressable', '~> 2.8' gem 'bootsnap', '~> 1.16.0', require: false gem 'browser' gem 'charlock_holmes', '~> 0.7.7' -gem 'chewy', '~> 7.3' +gem 'chewy', '~> 7.2' gem 'devise', '~> 4.9' -gem 'devise-two-factor', '~> 4.1' +gem 'devise-two-factor', '~> 4.0' group :pam_authentication, optional: true do gem 'devise_pam_authenticatable2', '~> 9.2' end -gem 'net-ldap', '~> 0.18' +gem 'net-ldap', '~> 0.17' gem 'omniauth-cas', '~> 2.0' gem 'omniauth-saml', '~> 1.10' gem 'omniauth_openid_connect', '~> 0.6.1' @@ -59,7 +59,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.15' +gem 'nokogiri', '~> 1.14' +gem 'nsa', '~> 0.2' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' @@ -68,14 +69,14 @@ gem 'public_suffix', '~> 5.0' gem 'pundit', '~> 2.3' gem 'premailer-rails' gem 'rack-attack', '~> 6.6' -gem 'rack-cors', '~> 2.0', require: 'rack/cors' +gem 'rack-cors', '~> 1.1', require: 'rack/cors' gem 'rails-i18n', '~> 6.0' gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true' gem 'redcarpet', '~> 3.6' gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis'] gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' -gem 'rqrcode', '~> 2.2' -gem 'ruby-progressbar', '~> 1.13' +gem 'rqrcode', '~> 2.1' +gem 'ruby-progressbar', '~> 1.11' gem 'sanitize', '~> 6.0' gem 'scenic', '~> 1.7' gem 'sidekiq', '~> 6.5' @@ -86,10 +87,10 @@ gem 'simple-navigation', '~> 4.4' gem 'simple_form', '~> 5.2' gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie' gem 'stoplight', '~> 3.0.1' -gem 'strong_migrations', '~> 0.8' +gem 'strong_migrations', '~> 0.7' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' -gem 'tzinfo-data', '~> 1.2023' +gem 'tzinfo-data', '~> 1.2022' gem 'webpacker', '~> 5.4' gem 'webpush', github: 'ClearlyClaire/webpush', ref: 'f14a4d52e201128b1b00245d11b6de80d6cfdcd9' gem 'webauthn', '~> 3.0' @@ -98,87 +99,54 @@ gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -gem 'private_address_check', '~> 0.5' - -group :test do - # RSpec runner for rails +group :development, :test do + gem 'fabrication', '~> 2.30' + gem 'fuubar', '~> 2.5' + gem 'i18n-tasks', '~> 1.0', require: false 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 - # 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 + gem 'rubocop', require: false +end - # Annotates modules with schema +group :production, :test do + gem 'private_address_check', '~> 0.5' +end + +group :test do + gem 'capybara', '~> 3.38' + gem 'climate_control' + gem 'faker', '~> 3.1' + gem 'json-schema', '~> 3.0' + gem 'rack-test', '~> 2.1' + gem 'rails-controller-testing', '~> 1.0' + gem 'rspec_junit_formatter', '~> 0.6' + gem 'rspec-sidekiq', '~> 3.1' + gem 'simplecov', '~> 0.22', require: false + gem 'webmock', '~> 3.18' +end + +group :development do gem 'annotate', '~> 3.2' - - # Enhanced error message pages for development gem 'better_errors', '~> 2.9' gem 'binding_of_caller', '~> 1.0' - - # Preview mail in the browser gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' - - # Security analysis CLI tools + gem 'memory_profiler' 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' - # Validate missing i18n keys - gem 'i18n-tasks', '~> 1.0', require: false - - # Profiling tools - gem 'memory_profiler', require: false - gem 'stackprof', require: false + gem 'stackprof' end group :production do @@ -189,9 +157,7 @@ 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 ad789db1e..87ab1cc5f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,18 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) +GIT + remote: https://github.com/kreeti/kt-paperclip.git + revision: 11abf222dc31bff71160a1d138b445214f434b2b + ref: 11abf222dc31bff71160a1d138b445214f434b2b + specs: + kt-paperclip (7.1.1) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + marcel (~> 1.0.1) + mime-types + terrapin (~> 0.6.0) + GIT remote: https://github.com/mastodon/rails-settings-cached.git revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab @@ -82,7 +94,7 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.4) + addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) aes_key_wrap (1.1.0) airbrussh (1.4.1) @@ -92,22 +104,22 @@ GEM activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) ast (2.4.2) - attr_encrypted (4.0.0) + attr_encrypted (3.1.0) encryptor (~> 3.0.0) attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.772.0) - aws-sdk-core (3.174.0) + aws-partitions (1.711.0) + aws-sdk-core (3.170.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.65.0) - aws-sdk-core (~> 3, >= 3.174.0) + aws-sdk-kms (1.62.0) + aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.123.0) - aws-sdk-core (~> 3, >= 3.174.0) + aws-sdk-s3 (1.119.1) + aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.5.2) @@ -130,7 +142,7 @@ GEM blurhash (0.1.7) bootsnap (1.16.0) msgpack (~> 1.2) - brakeman (5.4.1) + brakeman (5.4.0) browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -139,12 +151,12 @@ GEM bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) - capistrano (3.17.3) + capistrano (3.17.2) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (2.1.0) + capistrano-bundler (2.0.1) capistrano (~> 3.1) capistrano-rails (1.6.2) capistrano (~> 3.1) @@ -154,7 +166,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.39.1) + capybara (3.38.0) addressable matrix mini_mime (>= 0.1.3) @@ -167,7 +179,7 @@ GEM activesupport cbor (0.5.9.6) charlock_holmes (0.7.7) - chewy (7.3.2) + chewy (7.2.7) activesupport (>= 5.2) elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch-dsl @@ -177,26 +189,26 @@ GEM coderay (1.1.3) color_diff (0.1) concurrent-ruby (1.2.2) - connection_pool (2.4.1) + connection_pool (2.3.0) 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.14.0) + css_parser (1.12.0) addressable date (3.3.3) - debug_inspector (1.1.0) - devise (4.9.2) + debug_inspector (1.0.0) + devise (4.9.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (4.1.0) + devise-two-factor (4.0.2) activesupport (< 7.1) - attr_encrypted (>= 1.3, < 5, != 2) + attr_encrypted (>= 1.3, < 4, != 2) devise (~> 4.0) railties (< 7.1) rotp (~> 6.0) @@ -209,7 +221,7 @@ GEM docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.6.6) + doorkeeper (5.6.5) railties (>= 5) dotenv (2.8.1) dotenv-rails (2.8.1) @@ -229,9 +241,9 @@ GEM erubi (1.12.0) et-orbi (1.2.7) tzinfo - excon (0.99.0) + excon (0.95.0) fabrication (2.30.0) - faker (3.2.0) + faker (3.1.1) i18n (>= 1.8.11, < 2) faraday (1.10.3) faraday-em_http (~> 1.0) @@ -257,7 +269,7 @@ GEM faraday-rack (1.0.0) faraday-retry (1.0.3) fast_blank (1.0.1) - fastimage (2.2.7) + fastimage (2.2.6) ffi (1.15.5) ffi-compiler (1.0.1) ffi (>= 1.0.0) @@ -302,7 +314,7 @@ GEM hashie (5.0.0) hcaptcha (7.1.0) json - highline (2.1.0) + highline (2.0.3) hiredis (0.6.3) hkdf (0.3.0) htmlentities (4.3.4) @@ -319,7 +331,7 @@ GEM httplog (1.6.2) rack (>= 2.0) rainbow (>= 2.0.0) - i18n (1.13.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) i18n-tasks (1.0.12) activesupport (>= 4.0.2) @@ -336,23 +348,23 @@ GEM ipaddress (0.8.3) jmespath (1.6.2) json (2.6.3) - json-canonicalization (0.3.2) + json-canonicalization (0.3.0) json-jwt (1.15.3) activesupport (>= 4.2) aes_key_wrap bindata httpclient - json-ld (3.2.5) + json-ld (3.2.3) htmlentities (~> 4.3) - json-canonicalization (~> 0.3, >= 0.3.2) + json-canonicalization (~> 0.3) link_header (~> 0.0, >= 0.0.8) multi_json (~> 1.15) - rack (>= 2.2, < 4) - rdf (~> 3.2, >= 3.2.10) + rack (~> 2.2) + rdf (~> 3.2, >= 3.2.9) json-ld-preloaded (3.2.2) json-ld (~> 3.2) rdf (~> 3.2) - json-schema (4.0.0) + json-schema (3.0.0) addressable (>= 2.8) jsonapi-renderer (0.2.2) jwt (2.7.0) @@ -368,14 +380,8 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - 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.2) - addressable (~> 2.8) + launchy (2.5.0) + addressable (~> 2.7) letter_opener (1.8.1) launchy (>= 2.2, < 3) letter_opener_web (2.0.0) @@ -392,9 +398,9 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.21.3) + loofah (2.19.1) crass (~> 1.0.2) - nokogiri (>= 1.12.0) + nokogiri (>= 1.5.9) mail (2.8.1) mini_mime (>= 0.1.1) net-imap @@ -410,11 +416,11 @@ GEM method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) - mime-types-data (3.2023.0218.1) + mime-types-data (3.2022.0105) mini_mime (1.1.2) - mini_portile2 (2.8.2) + mini_portile2 (2.8.1) minitest (5.18.0) - msgpack (1.7.0) + msgpack (1.6.0) multi_json (1.15.0) multipart-post (2.3.0) net-http (0.3.2) @@ -422,7 +428,7 @@ GEM net-imap (0.3.4) date net-protocol - net-ldap (0.18.0) + net-ldap (0.17.1) net-pop (0.1.2) net-protocol net-protocol (0.2.1) @@ -431,12 +437,17 @@ GEM net-ssh (>= 2.6.5, < 8.0.0) net-smtp (0.3.3) net-protocol - net-ssh (7.1.0) - nio4r (2.5.9) - nokogiri (1.15.2) - mini_portile2 (~> 2.8.2) + net-ssh (7.0.1) + nio4r (2.5.8) + nokogiri (1.14.2) + mini_portile2 (~> 2.8.0) racc (~> 1.4) - oj (3.14.3) + 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.14.2) omniauth (1.9.2) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -468,20 +479,19 @@ GEM openssl-signature_algorithm (1.3.0) openssl (> 2.0) orm_adapter (0.5.0) - ox (2.14.16) - parallel (1.23.0) - parser (3.2.2.3) + ox (2.14.14) + parallel (1.22.1) + parser (3.2.1.1) ast (~> 2.4.1) - racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) - pg (1.5.3) - pghero (3.3.3) + pg (1.4.6) + pghero (3.3.0) activerecord (>= 6) pkg-config (1.5.1) posix-spawn (0.3.15) - premailer (1.21.0) + premailer (1.18.0) addressable css_parser (>= 1.12.0) htmlentities (>= 4.0.0) @@ -491,16 +501,16 @@ GEM premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) public_suffix (5.0.1) - puma (6.3.0) + puma (6.1.1) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.7.0) - rack (2.2.7) + racc (1.6.2) + rack (2.2.6.4) rack-attack (6.6.1) rack (>= 1.0, < 3) - rack-cors (2.0.1) + rack-cors (1.1.1) rack (>= 2.0.0) rack-oauth2 (1.21.3) activesupport @@ -534,9 +544,8 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) - loofah (~> 2.21) - nokogiri (~> 1.14) + rails-html-sanitizer (1.5.0) + loofah (~> 2.19, >= 2.19.1) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) @@ -548,7 +557,7 @@ GEM thor (~> 1.0) rainbow (3.1.1) rake (13.0.6) - rdf (3.2.10) + rdf (3.2.9) link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.5.1) rdf (~> 3.2) @@ -558,58 +567,60 @@ GEM redis (>= 4) redlock (1.3.2) redis (>= 3.0.0, < 6.0) - regexp_parser (2.8.1) + regexp_parser (2.7.0) request_store (1.5.1) rack (>= 1.4) responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) rexml (3.2.5) - rotp (6.2.2) + rotp (6.2.0) rpam2 (4.0.2) - rqrcode (2.2.0) + rqrcode (2.1.2) chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) - rspec-core (3.12.2) + rspec-core (3.12.1) rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-mocks (3.12.5) + rspec-mocks (3.12.3) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.3) + rspec-rails (6.0.1) 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-core (~> 3.11) + rspec-expectations (~> 3.11) + rspec-mocks (~> 3.11) + rspec-support (~> 3.11) rspec-sidekiq (3.1.0) rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.12.0) rspec_chunked (0.6) - rubocop (1.52.1) + rspec_junit_formatter (0.6.0) + rspec-core (>= 2, < 4, != 2.12.0) + rubocop (1.48.1) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.26.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.27.0) parser (>= 3.2.1.0) - rubocop-capybara (2.18.0) + rubocop-capybara (2.17.1) rubocop (~> 1.41) - rubocop-performance (1.18.0) + rubocop-performance (1.16.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.19.1) + rubocop-rails (2.18.0) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) @@ -621,7 +632,6 @@ GEM nokogiri (>= 1.10.5) rexml ruby2_keywords (0.0.5) - rubyzip (2.3.2) rufus-scheduler (3.8.2) fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) @@ -633,13 +643,13 @@ GEM activerecord (>= 4.0.0) railties (>= 4.0.0) semantic_range (3.0.0) - sidekiq (6.5.9) + sidekiq (6.5.8) connection_pool (>= 2.2.5, < 3) rack (~> 2.0) redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (5.0.3) + sidekiq-scheduler (5.0.2) rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0) @@ -671,11 +681,12 @@ GEM sshkit (1.21.4) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) - stackprof (0.2.25) + stackprof (0.2.23) + statsd-ruby (1.5.0) stoplight (3.0.1) redlock (~> 1.0) - strong_migrations (0.8.0) - activerecord (>= 5.2) + strong_migrations (0.7.9) + activerecord (>= 5) swd (1.3.0) activesupport (>= 3) attr_required (>= 0.0.5) @@ -686,7 +697,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - thor (1.2.2) + thor (1.2.1) tilt (2.1.0) timeout (0.3.2) tpm-key_attestation (0.12.0) @@ -708,13 +719,13 @@ GEM unf (~> 0.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - tzinfo-data (1.2023.3) + tzinfo-data (1.2022.7) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - uri (0.12.1) + uri (0.12.0) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) @@ -751,7 +762,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.8) + zeitwerk (2.6.7) PLATFORMS ruby @@ -760,7 +771,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.123) + aws-sdk-s3 (~> 1.119) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) @@ -772,23 +783,23 @@ DEPENDENCIES capistrano-rails (~> 1.6) capistrano-rbenv (~> 2.2) capistrano-yarn (~> 2.0) - capybara (~> 3.39) + capybara (~> 3.38) charlock_holmes (~> 0.7.7) - chewy (~> 7.3) - climate_control (~> 0.2) + chewy (~> 7.2) + climate_control cocoon (~> 1.2) color_diff (~> 0.1) concurrent-ruby connection_pool devise (~> 4.9) - devise-two-factor (~> 4.1) + devise-two-factor (~> 4.0) devise_pam_authenticatable2 (~> 9.2) discard (~> 1.2) doorkeeper (~> 5.6) dotenv-rails (~> 2.8) ed25519 (~> 1.3) fabrication (~> 2.30) - faker (~> 3.2) + faker (~> 3.1) fast_blank (~> 1.0) fastimage fog-core (<= 2.4.0) @@ -806,9 +817,9 @@ DEPENDENCIES idn-ruby json-ld json-ld-preloaded (~> 3.2) - json-schema (~> 4.0) + json-schema (~> 3.0) kaminari (~> 1.2) - kt-paperclip (~> 7.2) + kt-paperclip (~> 7.1)! letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) @@ -818,8 +829,9 @@ DEPENDENCIES memory_profiler mime-types (~> 3.4.1) net-http (~> 0.3.2) - net-ldap (~> 0.18) - nokogiri (~> 1.15) + net-ldap (~> 0.17) + nokogiri (~> 1.14) + nsa (~> 0.2) oj (~> 3.14) omniauth (~> 1.9) omniauth-cas (~> 2.0) @@ -828,18 +840,18 @@ DEPENDENCIES omniauth_openid_connect (~> 0.6.1) ox (~> 2.14) parslet - pg (~> 1.5) + pg (~> 1.4) pghero pkg-config (~> 1.5) posix-spawn premailer-rails private_address_check (~> 0.5) public_suffix (~> 5.0) - puma (~> 6.3) + puma (~> 6.1) pundit (~> 2.3) - rack (~> 2.2.7) + rack (~> 2.2.6) rack-attack (~> 6.6) - rack-cors (~> 2.0) + rack-cors (~> 1.1) rack-test (~> 2.1) rails (~> 6.1.7) rails-controller-testing (~> 1.0) @@ -849,17 +861,17 @@ DEPENDENCIES redcarpet (~> 3.6) redis (~> 4.5) redis-namespace (~> 1.10) - rqrcode (~> 2.2) + rqrcode (~> 2.1) rspec-rails (~> 6.0) rspec-sidekiq (~> 3.1) rspec_chunked (~> 0.6) + rspec_junit_formatter (~> 0.6) rubocop rubocop-capybara rubocop-performance rubocop-rails rubocop-rspec - ruby-progressbar (~> 1.13) - rubyzip (~> 2.3) + ruby-progressbar (~> 1.11) sanitize (~> 6.0) scenic (~> 1.7) sidekiq (~> 6.5) @@ -873,11 +885,11 @@ DEPENDENCIES sprockets-rails (~> 3.4) stackprof stoplight (~> 3.0.1) - strong_migrations (~> 0.8) + strong_migrations (~> 0.7) thor (~> 1.2) tty-prompt (~> 0.23) twitter-text (~> 3.1.0) - tzinfo-data (~> 1.2023) + tzinfo-data (~> 1.2022) webauthn (~> 3.0) webmock (~> 3.18) webpacker (~> 5.4) @@ -885,7 +897,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.2.2p53 + ruby 3.2.1p31 BUNDLED WITH - 2.4.13 + 2.4.6 diff --git a/README.md b/README.md index 4b32e7db5..9c9ffd9a8 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# Mastodon Glitch+Urusai Edition # +# Mastodon Glitch, Urusai+Sakurajima Edition -This is the repo for the fork of Mastodon used by [Urusai! Social](https://urusai.social/). +This is the repo for the fork of Mastodon used by [Sakurajima](https://sakurajima.moe) which is a fork of[Urusai! Social](https://urusai.social/). This repo exists so I don't have to copy and paste the customizations when I update Mastodon. -Added features: +# Added features from [Urusai fork](https://github.com/neatchee/mastodon): - Flavours/styles from @chikorita157@sakurajima.moe - Emoji enhancements - Enlarge emoji in post content on mouse-over/tap -- Collapsed post improvements +- Collapsed post improvements - show > 1 line for better visual parsing - customize height of posts to trigger lengthy toot collapse, etc) - Option to merge boosts and favorites into a single notification for the same status @@ -18,3 +18,10 @@ So here's the deal: we all work on this code, and anyone who uses that does so a - You can view documentation for the original glitch-soc project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/). - And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/). + +# Our Additional Changes +- Add Mastodon Modern Theme by [Freeplay](https://codeberg.org/Freeplay/Mastodon-Modern) with color variants we have already +- Footer links and login tweaks +- Holiday Themes Added +- Mastodon-bird-ui theme by [ronilaukkarinen](https://github.com/ronilaukkarinen/mastodon-bird-ui/blob/master/style.css) +- Add Quote Toots (cherrypicked from Treehouse fork by [Ariadne Conill ](https://gitea.treehouse.systems/treehouse/mastodon/pulls/36)) diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index c4b7e9c9d..104348614 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(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + expires_in 0, public: true unless user_signed_in? end private diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 929bb54aa..4d03a04b7 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,9 +7,8 @@ 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? @@ -17,7 +16,7 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do - expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? + expires_in 0, public: true 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 388d4b9e1..b8a7e0ab9 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -7,6 +7,10 @@ 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 4ed59388f..23d874071 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -4,12 +4,11 @@ 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 976caa344..4e445bcb1 100644 --- a/app/controllers/activitypub/followers_synchronizations_controller.rb +++ b/app/controllers/activitypub/followers_synchronizations_controller.rb @@ -4,10 +4,9 @@ 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 bf10ba762..60d201f76 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -6,10 +6,9 @@ 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? @@ -17,7 +16,6 @@ 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 @@ -82,4 +80,8 @@ 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 c38ff89d1..8e0f9de2e 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -7,10 +7,9 @@ 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 8f9708183..351b9a991 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -14,10 +14,6 @@ class Admin::AnnouncementsController < Admin::BaseController @announcement = Announcement.new end - def edit - authorize :announcement, :update? - end - def create authorize :announcement, :create? @@ -32,6 +28,10 @@ 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 a71bb6129..c645ce12b 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -9,8 +9,6 @@ module Admin before_action :set_pack before_action :set_body_classes - before_action :set_cache_headers - after_action :verify_authorized private @@ -23,10 +21,6 @@ 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 3a6df662e..099512248 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -14,5 +14,15 @@ module Admin @pending_tags_count = Tag.pending_review.count @pending_appeals_count = Appeal.pending.count end + + private + + def redis_info + @redis_info ||= if redis.is_a?(Redis::Namespace) + redis.redis.info + else + redis.info + end + end end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index b9691c5a3..060db11bb 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: [:destroy, :edit, :update] + before_action :set_domain_block, only: [:show, :destroy, :edit, :update] def batch authorize :domain_block, :create? @@ -31,41 +31,31 @@ 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 + 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 @domain_block.errors.delete(:domain) - return 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 + else + if existing_domain_block.present? + @domain_block = existing_domain_block + @domain_block.update(resource_params) + 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 end end def update authorize :domain_block, :update? - @domain_block.assign_attributes(update_params) - - # Require explicit confirmation when suspending - return render :confirm_suspension if requires_confirmation? - - if @domain_block.save + if @domain_block.update(update_params) 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') @@ -102,9 +92,5 @@ module Admin def action_from_button '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 4a3228ec3..a0a43de19 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -2,6 +2,8 @@ module Admin class EmailDomainBlocksController < BaseController + before_action :set_email_domain_block, only: [:show, :destroy] + def index authorize :email_domain_block, :index? @@ -57,6 +59,10 @@ 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 bcfc11159..d76aa745b 100644 --- a/app/controllers/admin/roles_controller.rb +++ b/app/controllers/admin/roles_controller.rb @@ -16,10 +16,6 @@ module Admin @role = UserRole.new end - def edit - authorize @role, :update? - end - def create authorize :user_role, :create? @@ -34,6 +30,10 @@ 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 d31aec6ea..f3bed3ad8 100644 --- a/app/controllers/admin/rules_controller.rb +++ b/app/controllers/admin/rules_controller.rb @@ -11,10 +11,6 @@ module Admin @rule = Rule.new end - def edit - authorize @rule, :update? - end - def create authorize :rule, :create? @@ -28,6 +24,10 @@ 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 efbf65b11..b376f8d9b 100644 --- a/app/controllers/admin/warning_presets_controller.rb +++ b/app/controllers/admin/warning_presets_controller.rb @@ -11,10 +11,6 @@ module Admin @warning_preset = AccountWarningPreset.new end - def edit - authorize @warning_preset, :update? - end - def create authorize :account_warning_preset, :create? @@ -28,6 +24,10 @@ 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 01d9ba8ce..d6fb1a4ea 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -10,20 +10,12 @@ 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? @@ -36,6 +28,14 @@ module Admin end end + def show + authorize @webhook, :show? + end + + def edit + authorize @webhook, :update? + end + def update authorize @webhook, :update? @@ -71,7 +71,7 @@ module Admin end def resource_params - params.require(:webhook).permit(:url, :template, events: []) + params.require(:webhook).permit(:url, events: []) end end end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 2629ab782..41f3ce2ee 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -6,14 +6,13 @@ 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! - - vary_by 'Authorization' + before_action :set_cache_headers protect_from_forgery with: :null_session @@ -149,6 +148,10 @@ 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 7c7d70fd3..94b707771 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) - current_user.update(user_params) if user_params + UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params ActivityPub::UpdateDistributionWorker.perform_async(@account.id) render json: @account, serializer: REST::CredentialAccountSerializer end @@ -34,17 +34,15 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController ) end - def user_params + def user_settings_params return nil if params[:source].blank? source_params = params.require(:source) { - 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), - }, + '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), } 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 1a996d362..68952de89 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -6,7 +6,6 @@ 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 diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 6e6ebae43..0a4d2ae7b 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -6,7 +6,6 @@ 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 diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb index 6d6339878..8597f891d 100644 --- a/app/controllers/api/v1/accounts/lookup_controller.rb +++ b/app/controllers/api/v1/accounts/lookup_controller.rb @@ -5,7 +5,6 @@ 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 51f541bd2..7ed48cf65 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -7,7 +7,6 @@ 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 diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index ddb94d5ca..7dff66efa 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -18,7 +18,6 @@ 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 @@ -90,7 +89,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone) + params.permit(:username, :email, :password, :agreement, :locale, :reason) end def check_enabled_registrations 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 7b192b979..9ef1b3be7 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.require(:email)) + @canonical_email_blocks = CanonicalEmailBlock.matching_email(params[: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 dd54d6710..0658199f0 100644 --- a/app/controllers/api/v1/admin/domain_allows_controller.rb +++ b/app/controllers/api/v1/admin/domain_allows_controller.rb @@ -16,6 +16,19 @@ 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 @@ -26,19 +39,6 @@ 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 2538c7c7c..8b77e9717 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -16,16 +16,6 @@ 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? @@ -38,6 +28,16 @@ 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 850eda622..e53d0b157 100644 --- a/app/controllers/api/v1/admin/email_domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/email_domain_blocks_controller.rb @@ -18,6 +18,15 @@ 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 @@ -28,15 +37,6 @@ 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 61c191234..201ab6b1f 100644 --- a/app/controllers/api/v1/admin/ip_blocks_controller.rb +++ b/app/controllers/api/v1/admin/ip_blocks_controller.rb @@ -18,6 +18,13 @@ 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 @@ -28,13 +35,6 @@ 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_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb index 7f4ca4828..cc6388980 100644 --- a/app/controllers/api/v1/admin/trends/links_controller.rb +++ b/app/controllers/api/v1/admin/trends/links_controller.rb @@ -1,36 +1,7 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::LinksController < Api::V1::Trends::LinksController - 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 + before_action -> { authorize_if_got_token! :'admin:read' } private diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb index 34b6580df..c39f77363 100644 --- a/app/controllers/api/v1/admin/trends/statuses_controller.rb +++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb @@ -1,36 +1,7 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::StatusesController < Api::V1::Trends::StatusesController - 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 + before_action -> { authorize_if_got_token! :'admin:read' } private diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb index 2eeea9522..e77df3021 100644 --- a/app/controllers/api/v1/admin/trends/tags_controller.rb +++ b/app/controllers/api/v1/admin/trends/tags_controller.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true class Api::V1::Admin::Trends::TagsController < Api::V1::Trends::TagsController - 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 + before_action -> { authorize_if_got_token! :'admin:read' } def index if current_user&.can?(:manage_taxonomies) @@ -16,22 +11,6 @@ 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/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 63644f85e..9034e8a2f 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, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id) + render json: @conversations, each_serializer: REST::ConversationSerializer end def read @@ -32,19 +32,6 @@ 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 diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb index 4c056c952..08b3474cc 100644 --- a/app/controllers/api/v1/custom_emojis_controller.rb +++ b/app/controllers/api/v1/custom_emojis_controller.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true class Api::V1::CustomEmojisController < Api::BaseController - vary_by '', unless: :disallow_unauthenticated_api_access? - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :set_cache_headers def index - cache_even_if_authenticated! unless disallow_unauthenticated_api_access? + expires_in 3.minutes, public: true render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) } end end diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb index c0585e859..c91543e3a 100644 --- a/app/controllers/api/v1/directories_controller.rb +++ b/app/controllers/api/v1/directories_controller.rb @@ -5,7 +5,6 @@ class Api::V1::DirectoriesController < Api::BaseController before_action :set_accounts def show - cache_if_unauthenticated! render json: @accounts, each_serializer: REST::AccountSerializer end diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb index 29ff897b9..32fb8e39f 100644 --- a/app/controllers/api/v1/emails/confirmations_controller.rb +++ b/app/controllers/api/v1/emails/confirmations_controller.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true class Api::V1::Emails::ConfirmationsController < Api::BaseController - 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 -> { doorkeeper_authorize! :write, :'write:accounts' } + before_action :require_user_owned_by_application! + before_action :require_user_not_confirmed! def create current_user.update!(email: params[:email]) if params.key?(:email) @@ -13,10 +12,6 @@ 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! diff --git a/app/controllers/api/v1/featured_tags_controller.rb b/app/controllers/api/v1/featured_tags_controller.rb index 516046f00..edb42a94e 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, params.require(:name)) + featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name]) render json: featured_tag, serializer: REST::FeaturedTagSerializer end @@ -31,4 +31,8 @@ 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 ed98acce3..772791b25 100644 --- a/app/controllers/api/v1/filters_controller.rb +++ b/app/controllers/api/v1/filters_controller.rb @@ -11,10 +11,6 @@ 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) @@ -24,6 +20,10 @@ 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/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index 3d55d990a..bad61425a 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -3,12 +3,11 @@ 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 - cache_even_if_authenticated! + expires_in 1.day, public: true 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 e954c4589..37a6906fb 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -6,15 +6,8 @@ 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 - if Setting.show_domain_blocks == 'all' - cache_even_if_authenticated! - else - cache_if_unauthenticated! - end - + expires_in 3.minutes, public: true 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 a0665725b..c72e16cff 100644 --- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -2,19 +2,11 @@ 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 - cache_even_if_authenticated! + expires_in 3.minutes, public: true 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 70281362a..2877fec52 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -3,18 +3,11 @@ 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 - cache_even_if_authenticated! + expires_in 1.day, public: true 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 36889f733..dbd69f54d 100644 --- a/app/controllers/api/v1/instances/privacy_policies_controller.rb +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -5,10 +5,8 @@ class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController before_action :set_privacy_policy - vary_by '' - def show - cache_even_if_authenticated! + expires_in 1.day, public: true 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 d3eeca326..93cf3c759 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -2,19 +2,10 @@ 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 index c4680cccb..3910a499e 100644 --- a/app/controllers/api/v1/instances/translation_languages_controller.rb +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -5,10 +5,8 @@ class Api::V1::Instances::TranslationLanguagesController < Api::BaseController before_action :set_languages - vary_by '' - def show - cache_even_if_authenticated! + expires_in 1.day, public: true render json: @languages end diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 5a6701ff9..913319a86 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,18 +1,11 @@ # 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 - cache_even_if_authenticated! + expires_in 3.minutes, public: true render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance' end end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 4bbbed267..843ca2ec2 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, :exclusive) + params.permit(:title, :replies_policy) end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 5ea26d55b..f9c935bf3 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -6,20 +6,19 @@ 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 => e - Rails.logger.error "#{e.class}: #{e.message}" + rescue Paperclip::Error 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/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index ffc70a849..6435e9f0d 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -8,7 +8,6 @@ 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 3634acf95..7148d63a4 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -6,10 +6,6 @@ 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! @@ -25,6 +21,10 @@ 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/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 73eb11e71..b138fa265 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -8,7 +8,6 @@ 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 diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index dff2425d0..7fe73a6f5 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -7,7 +7,6 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController before_action :set_status def show - cache_if_unauthenticated! render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer end diff --git a/app/controllers/api/v1/statuses/reactions_controller.rb b/app/controllers/api/v1/statuses/reactions_controller.rb index 2d7e4f598..333054f2a 100644 --- a/app/controllers/api/v1/statuses/reactions_controller.rb +++ b/app/controllers/api/v1/statuses/reactions_controller.rb @@ -9,23 +9,17 @@ class Api::V1::Statuses::ReactionsController < Api::BaseController def create ReactService.new.call(current_account, @status, params[:id]) - render json: @status, serializer: REST::StatusSerializer + render_empty end def destroy - UnreactWorker.perform_async(current_account.id, @status.id, params[:id]) - - render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reactions_map: { @status.id => false }) - rescue Mastodon::NotPermittedError - not_found + UnreactService.new.call(current_account, @status, params[:id]) + render_empty end private def set_status @status = Status.find(params[:status_id]) - authorize @status, :show? - rescue Mastodon::NotPermittedError - not_found end end 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 41672e753..4b545f982 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -8,7 +8,6 @@ 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 diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index e3769437b..1be15a5a4 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -2,8 +2,6 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController include Authorization - include Redisable - include Lockable before_action -> { doorkeeper_authorize! :write, :'write:statuses' } before_action :require_user! @@ -12,9 +10,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController override_rate_limit_headers :create, family: :statuses def create - with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do - @status = ReblogService.new.call(current_account, @reblog, reblog_params) - end + @status = ReblogService.new.call(current_account, @reblog, reblog_params) 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 960f8cf76..f69418c37 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -24,14 +24,11 @@ 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 @@ -69,7 +66,8 @@ class Api::V1::StatusesController < Api::BaseController content_type: status_params[:content_type], allowed_mentions: status_params[:allowed_mentions], idempotency: request.headers['Idempotency-Key'], - with_rate_limit: true + with_rate_limit: true, + quote_id: status_params[:quote_id].presence ) render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer @@ -140,6 +138,7 @@ class Api::V1::StatusesController < Api::BaseController :visibility, :language, :scheduled_at, + :quote_id, :content_type, allowed_mentions: [], media_ids: [], diff --git a/app/controllers/api/v1/streaming_controller.rb b/app/controllers/api/v1/streaming_controller.rb index 0cdd00d62..b23a60170 100644 --- a/app/controllers/api/v1/streaming_controller.rb +++ b/app/controllers/api/v1/streaming_controller.rb @@ -5,7 +5,7 @@ class Api::V1::StreamingController < Api::BaseController if Rails.configuration.x.streaming_api_base_url == request.host not_found else - redirect_to streaming_api_url, status: 301, allow_other_host: true + redirect_to streaming_api_url, status: 301 end end diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb index 284ec8593..a08fd2187 100644 --- a/app/controllers/api/v1/tags_controller.rb +++ b/app/controllers/api/v1/tags_controller.rb @@ -8,7 +8,6 @@ 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 diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index bb3bbdc21..220b31b36 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -5,7 +5,6 @@ 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 diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 9cd7b9904..64a1db58d 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -5,7 +5,6 @@ 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 57cfa0b7e..3ce20fb78 100644 --- a/app/controllers/api/v1/trends/links_controller.rb +++ b/app/controllers/api/v1/trends/links_controller.rb @@ -1,8 +1,6 @@ # 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 @@ -10,7 +8,6 @@ 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 diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb index c186864c3..3aab92477 100644 --- a/app/controllers/api/v1/trends/statuses_controller.rb +++ b/app/controllers/api/v1/trends/statuses_controller.rb @@ -1,14 +1,11 @@ # 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 diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb index 6cc8194de..9dd9abdfe 100644 --- a/app/controllers/api/v1/trends/tags_controller.rb +++ b/app/controllers/api/v1/trends/tags_controller.rb @@ -8,7 +8,6 @@ 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 diff --git a/app/controllers/api/v2/filters/keywords_controller.rb b/app/controllers/api/v2/filters/keywords_controller.rb index fe1a99194..c63e1d986 100644 --- a/app/controllers/api/v2/filters/keywords_controller.rb +++ b/app/controllers/api/v2/filters/keywords_controller.rb @@ -12,16 +12,16 @@ class Api::V2::Filters::KeywordsController < Api::BaseController render json: @keywords, each_serializer: REST::FilterKeywordSerializer end - def show - render json: @keyword, serializer: REST::FilterKeywordSerializer - end - def create @keyword = current_account.custom_filters.find(params[:filter_id]).keywords.create!(resource_params) render json: @keyword, serializer: REST::FilterKeywordSerializer end + def show + render json: @keyword, serializer: REST::FilterKeywordSerializer + end + def update @keyword.update!(resource_params) diff --git a/app/controllers/api/v2/filters/statuses_controller.rb b/app/controllers/api/v2/filters/statuses_controller.rb index 2e95497a6..755c14cff 100644 --- a/app/controllers/api/v2/filters/statuses_controller.rb +++ b/app/controllers/api/v2/filters/statuses_controller.rb @@ -12,16 +12,16 @@ class Api::V2::Filters::StatusesController < Api::BaseController render json: @status_filters, each_serializer: REST::FilterStatusSerializer end - def show - render json: @status_filter, serializer: REST::FilterStatusSerializer - end - def create @status_filter = current_account.custom_filters.find(params[:filter_id]).statuses.create!(resource_params) render json: @status_filter, serializer: REST::FilterStatusSerializer end + def show + render json: @status_filter, serializer: REST::FilterStatusSerializer + end + def destroy @status_filter.destroy! render_empty diff --git a/app/controllers/api/v2/filters_controller.rb b/app/controllers/api/v2/filters_controller.rb index 2fcdeeae4..8ff3076cf 100644 --- a/app/controllers/api/v2/filters_controller.rb +++ b/app/controllers/api/v2/filters_controller.rb @@ -11,16 +11,16 @@ class Api::V2::FiltersController < Api::BaseController render json: @filters, each_serializer: REST::FilterSerializer, rules_requested: true end - def show - render json: @filter, serializer: REST::FilterSerializer, rules_requested: true - end - def create @filter = current_account.custom_filters.create!(resource_params) render json: @filter, serializer: REST::FilterSerializer, rules_requested: true end + def show + render json: @filter, serializer: REST::FilterSerializer, rules_requested: true + end + def update @filter.update!(resource_params) diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb index 8346e2883..bcd90cff2 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 - cache_even_if_authenticated! + expires_in 3.minutes, public: true 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 72bc69442..288f847f1 100644 --- a/app/controllers/api/v2/media_controller.rb +++ b/app/controllers/api/v2/media_controller.rb @@ -6,8 +6,7 @@ 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 => e - Rails.logger.error "#{e.class}: #{e.message}" + rescue Paperclip::Error render json: processing_error, status: 500 end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7c09040fb..906761f6f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -21,8 +21,6 @@ class ApplicationController < ActionController::Base 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 @@ -39,11 +37,9 @@ class ApplicationController < ActionController::Base service_unavailable end - before_action :store_referrer, except: :raise_not_found, if: :devise_controller? + before_action :store_current_location, except: :raise_not_found, unless: :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 @@ -60,25 +56,14 @@ class ApplicationController < ActionController::Base !authorized_fetch_mode? end - 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) + def store_current_location + store_location_for(:user, request.url) unless [:json, :rss].include?(request.format&.to_sym) 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) if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true' '/auth/auth/openid_connect/logout' @@ -142,7 +127,7 @@ class ApplicationController < ActionController::Base end def sso_account_settings - ENV.fetch('SSO_ACCOUNT_SETTINGS', nil) + ENV.fetch('SSO_ACCOUNT_SETTINGS') end def current_account @@ -157,10 +142,6 @@ 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 @@ -170,8 +151,4 @@ 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 3283c5f36..0817a905c 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -15,6 +15,12 @@ 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 @@ -23,12 +29,6 @@ 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,12 +51,14 @@ 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 @@ -88,10 +90,8 @@ 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 - new_user_session_path + super end end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index a9d92b6e2..83e784e4c 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -25,16 +25,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(&:build_invite_request) end + def destroy + not_found + end + def update super do |resource| 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) @@ -47,7 +47,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(hash) resource.locale = I18n.locale - resource.invite_code = @invite&.code if resource.invite_code.blank? + resource.invite_code = params[:invite_code] if resource.invite_code.blank? resource.registration_form_time = session[:registration_form_time] resource.sign_up_ip = request.remote_ip @@ -132,7 +132,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_sessions - @sessions = current_user.session_activations.order(updated_at: :desc) + @sessions = current_user.session_activations end def set_strikes @@ -157,6 +157,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController end def set_cache_headers - response.cache_control.replace(private: true, no_store: true) + response.headers['Cache-Control'] = 'private, no-store' end end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 8edca4d01..db5a866f2 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -11,7 +11,15 @@ class Auth::SetupController < ApplicationController skip_before_action :require_functional! - def show; end + 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 update # This allows updating the e-mail without entering a password as is required @@ -19,13 +27,14 @@ class Auth::SetupController < ApplicationController # that were not confirmed yet if @user.update(user_params) - @user.resend_confirmation_instructions unless @user.confirmed? - redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent') + redirect_to auth_setup_path, notice: I18n.t('devise.confirmations.send_instructions') else render :show end end + helper_method :missing_email? + private def require_unconfirmed_or_pending! @@ -44,7 +53,11 @@ class Auth::SetupController < ApplicationController params.require(:user).permit(:email) end + def missing_email? + truthy_param?(:missing_email) + end + def set_pack - use_pack 'sign_up' + use_pack 'auth' end end diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index 73f0f2b88..97fe4a9ab 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, '').delete_prefix('acct:') + params[:uri] || params.fetch(:acct, '').gsub(/\Aacct:/, '') end def set_body_classes diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 205df48d4..2f4b400b8 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -11,15 +11,11 @@ class BackupsController < ApplicationController def download case Paperclip::Attachment.default_options[:storage] when :s3 - redirect_to @backup.dump.expiring_url(10), allow_other_host: true + redirect_to @backup.dump.expiring_url(10) 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 + redirect_to @backup.dump.expiring_url(Time.now.utc + 10) when :filesystem - redirect_to full_asset_url(@backup.dump.url), allow_other_host: true + redirect_to full_asset_url(@backup.dump.url) end end diff --git a/app/controllers/concerns/account_controller_concern.rb b/app/controllers/concerns/account_controller_concern.rb index e9cff22ca..2f7d84df0 100644 --- a/app/controllers/concerns/account_controller_concern.rb +++ b/app/controllers/concerns/account_controller_concern.rb @@ -10,8 +10,7 @@ module AccountControllerConcern included do before_action :set_instance_presenter - - after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } + before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html } end private diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 55ebe1bd6..e606218ac 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -155,30 +155,8 @@ module CacheConcern 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 @@ -198,6 +176,10 @@ 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) @@ -205,7 +187,7 @@ module CacheConcern return [] if raw.empty? cached_keys_with_value = begin - Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) } + Rails.cache.read_multi(*raw, namespace: 'v2').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 @@ -218,7 +200,7 @@ module CacheConcern uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id) uncached.each_value do |item| - Rails.cache.write(item, ActiveRecordCoder.dump(item)) + Rails.cache.write(item, ActiveRecordCoder.dump(item), namespace: 'v2') end end diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb index 576304d1c..538c1ffb1 100644 --- a/app/controllers/concerns/captcha_concern.rb +++ b/app/controllers/concerns/captcha_concern.rb @@ -2,7 +2,6 @@ module CaptchaConcern extend ActiveSupport::Concern - include Hcaptcha::Adapters::ViewMethods included do @@ -36,22 +35,18 @@ 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/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 1d27c92c8..931725943 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -180,15 +180,14 @@ module SignatureVerification def build_signed_string signed_headers.map do |signed_header| - case signed_header - when Request::REQUEST_TARGET + if signed_header == Request::REQUEST_TARGET "#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}" - when '(created)' + elsif signed_header == '(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']}" - when '(expires)' + elsif signed_header == '(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? @@ -245,7 +244,7 @@ module SignatureVerification end if key_id.start_with?('acct:') - stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) } + stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, ''), 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/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 96c31566e..7ba7a57e3 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -7,12 +7,6 @@ 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 e7a02ea89..9270c467d 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -1,8 +1,18 @@ # frozen_string_literal: true -class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController +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 + 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 f51f44c62..7830c5524 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -10,7 +10,6 @@ class Disputes::BaseController < ApplicationController before_action :set_body_classes before_action :authenticate_user! before_action :set_pack - before_action :set_cache_headers private @@ -21,8 +20,4 @@ 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 72bc56de0..41f1e1c5c 100644 --- a/app/controllers/emojis_controller.rb +++ b/app/controllers/emojis_controller.rb @@ -2,12 +2,15 @@ class EmojisController < ApplicationController before_action :set_emoji - - vary_by -> { 'Signature' if authorized_fetch_mode? } + before_action :set_cache_headers def show - expires_in 3.minutes, public: true - render_with_cache json: @emoji, content_type: 'application/activity+json', serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter + 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 end private diff --git a/app/controllers/filters/statuses_controller.rb b/app/controllers/filters/statuses_controller.rb index 97206c7ed..86d11fcb9 100644 --- a/app/controllers/filters/statuses_controller.rb +++ b/app/controllers/filters/statuses_controller.rb @@ -8,7 +8,6 @@ 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 @@ -50,8 +49,4 @@ class Filters::StatusesController < 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/filters_controller.rb b/app/controllers/filters_controller.rb index 180ddf070..2ab3b0a74 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -7,7 +7,6 @@ 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) @@ -18,8 +17,6 @@ class FiltersController < ApplicationController @filter.keywords.build end - def edit; end - def create @filter = current_account.custom_filters.build(resource_params) @@ -30,6 +27,8 @@ class FiltersController < ApplicationController end end + def edit; end + def update if @filter.update(resource_params) redirect_to filters_path @@ -60,8 +59,4 @@ 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 4b77945d1..bded2c280 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -5,9 +5,8 @@ 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? @@ -15,7 +14,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? + expires_in 0, public: true 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 2aa31bdf0..febd13c97 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -5,9 +5,8 @@ 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? @@ -15,7 +14,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do - expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? + expires_in 0, public: true unless user_signed_in? end format.json do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index ee940e670..d8ee82a7a 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(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + expires_in 0, public: true unless user_signed_in? end private diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 8422d74bc..0853897f2 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true -class InstanceActorsController < ActivityPub::BaseController - vary_by '' +class InstanceActorsController < ApplicationController + include AccountControllerConcern - serialization_scope nil - - before_action :set_account - skip_before_action :require_functional! - skip_before_action :update_user_sign_in + skip_before_action :check_account_confirmation + skip_around_action :set_locale def show expires_in 10.minutes, public: true diff --git a/app/controllers/intents_controller.rb b/app/controllers/intents_controller.rb index ea024e30e..ca89fc7fe 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'].delete_prefix('acct:')) + return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, '')) 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 2db4bc5cb..0b3c082dc 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -8,7 +8,6 @@ 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? @@ -55,8 +54,4 @@ 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/manifests_controller.rb b/app/controllers/manifests_controller.rb index 4fba9198f..960510f60 100644 --- a/app/controllers/manifests_controller.rb +++ b/app/controllers/manifests_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -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 +class ManifestsController < ApplicationController + skip_before_action :store_current_location + skip_before_action :require_functional! def show expires_in 3.minutes, public: true diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index ac820e92b..37c5dcb99 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -3,6 +3,7 @@ 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? @@ -46,7 +47,7 @@ class MediaController < ApplicationController end def allow_iframing - response.headers.delete('X-Frame-Options') + response.headers['X-Frame-Options'] = 'ALLOWALL' end def set_pack diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 8d480d704..3b228722f 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -6,6 +6,7 @@ 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? @@ -16,13 +17,13 @@ class MediaProxyController < ApplicationController rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error def show - with_redis_lock("media_download:#{params[:id]}") do + with_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)), allow_other_host: true + redirect_to full_asset_url(@media_attachment.file.url(version)) end private diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 62fc9c1b0..d6e7d0800 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.cache_control.replace(private: true, no_store: true) + response.headers['Cache-Control'] = 'private, no-store' end end diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 0a1df5506..b2564a791 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -8,9 +8,6 @@ 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! @@ -38,18 +35,4 @@ 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 070ee8a06..2c98bf3bf 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(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in? + expires_in 0, public: true if current_account.nil? end private diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index f83098f73..52cf1e0c1 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -8,7 +8,6 @@ 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? @@ -76,8 +75,4 @@ 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 d4b720568..e6e137c2b 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -8,8 +8,6 @@ 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, @@ -17,6 +15,8 @@ class Settings::ApplicationsController < Settings::BaseController ) end + def show; end + def create @application = current_user.applications.build(application_params) diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index 56aeb49aa..bf17b918c 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.cache_control.replace(private: true, no_store: true) + response.headers['Cache-Control'] = 'private, no-store' end def require_not_suspended! diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index 46a340aeb..deaa7940e 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_redis_lock("backup:#{current_user.id}") do + with_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 b179b9429..62c52eee9 100644 --- a/app/controllers/settings/flavours_controller.rb +++ b/app/controllers/settings/flavours_controller.rb @@ -12,15 +12,27 @@ class Settings::FlavoursController < Settings::BaseController end def show - redirect_to action: 'show', flavour: current_flavour unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) + unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) + redirect_to action: 'show', flavour: current_flavour + end @listing = Themes.instance.flavours @selected = params[:flavour] end def update - current_user.settings.update(flavour: params.require(:flavour), skin: params.dig(:user, :setting_skin)) - current_user.save + user_settings.update(user_settings_params) 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 983caf22f..d4516526e 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,101 +1,31 @@ # frozen_string_literal: true -require 'csv' - class Settings::ImportsController < Settings::BaseController - before_action :set_bulk_import, only: [:show, :confirm, :destroy] - before_action :set_recent_imports, only: [:index] + before_action :set_account - 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') + def show + @import = Import.new end def create - @import = Form::Import.new(import_params.merge(current_account: current_account)) + @import = Import.new(import_params) + @import.account = @account if @import.save - redirect_to settings_import_path(@import.bulk_import.id) + ImportWorker.perform_async(@import.id) + redirect_to settings_import_path, notice: I18n.t('imports.success') else - # We need to set recent imports as we are displaying the index again - set_recent_imports - render :index + render :show end end - def destroy - @bulk_import.destroy! - redirect_to settings_imports_path - end - private + def set_account + @account = current_user.account + end + def import_params - params.require(:form_import).permit(:data, :type, :mode) - end - - 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) + params.require(:import).permit(:data, :type, :mode) end end diff --git a/app/controllers/settings/preferences/appearance_controller.rb b/app/controllers/settings/preferences/appearance_controller.rb index 4d7d12bb7..80ea57bd2 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::Preferences::BaseController +class Settings::Preferences::AppearanceController < Settings::PreferencesController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences/notifications_controller.rb b/app/controllers/settings/preferences/notifications_controller.rb index 66d6c9a2f..a16ae6a67 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::Preferences::BaseController +class Settings::Preferences::NotificationsController < Settings::PreferencesController 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 a19fbf5c4..07eb89a76 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::Preferences::BaseController +class Settings::Preferences::OtherController < Settings::PreferencesController private def after_update_redirect_path diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb new file mode 100644 index 000000000..39715b724 --- /dev/null +++ b/app/controllers/settings/preferences_controller.rb @@ -0,0 +1,66 @@ +# 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_visible_reactions, + :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 0bff01ec2..cbba842a9 100644 --- a/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb +++ b/app/controllers/settings/two_factor_authentication/otp_authentication_controller.rb @@ -22,9 +22,18 @@ 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 3f9e71357..5a9029a42 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,10 @@ module Settings before_action :require_otp_enabled before_action :require_webauthn_enabled, only: [:index, :destroy] - def index; end def new; end + def index; end + def options current_user.update(webauthn_id: WebAuthn.generate_user_id) unless current_user.webauthn_id diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index 3ed1860a0..0e7bb835f 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -7,7 +7,6 @@ class StatusesCleanupController < ApplicationController before_action :set_policy before_action :set_body_classes before_action :set_pack - before_action :set_cache_headers def show; end @@ -42,8 +41,4 @@ 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 0efafb845..e5221df3a 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -6,16 +6,14 @@ 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? @@ -30,7 +28,7 @@ class StatusesController < ApplicationController end format.json do - expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? + expires_in 3.minutes, public: @status.distributable? && public_fetch_mode? render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end @@ -46,7 +44,7 @@ class StatusesController < ApplicationController return not_found if @status.hidden? || @status.reblog? expires_in 180, public: true - response.headers.delete('X-Frame-Options') + response.headers['X-Frame-Options'] = 'ALLOWALL' render layout: 'embedded' end @@ -73,6 +71,6 @@ class StatusesController < ApplicationController end def redirect_to_original - redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog? + redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog? end end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 7e249dbea..4b747c9ad 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -7,8 +7,6 @@ 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 @@ -21,7 +19,7 @@ class TagsController < ApplicationController def show respond_to do |format| format.html do - expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in? + expires_in 0, public: true unless user_signed_in? end format.rss do diff --git a/app/controllers/well_known/host_meta_controller.rb b/app/controllers/well_known/host_meta_controller.rb index 201da9fbc..2fd6bc7cc 100644 --- a/app/controllers/well_known/host_meta_controller.rb +++ b/app/controllers/well_known/host_meta_controller.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true module WellKnown - class HostMetaController < ActionController::Base # rubocop:disable Rails/ApplicationController + class HostMetaController < ActionController::Base 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 e20e8c62a..11a699ebc 100644 --- a/app/controllers/well_known/nodeinfo_controller.rb +++ b/app/controllers/well_known/nodeinfo_controller.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true module WellKnown - class NodeInfoController < ActionController::Base # rubocop:disable Rails/ApplicationController + class NodeInfoController < ActionController::Base include CacheConcern - # Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user` - # and thus re-issuing session cookies - serialization_scope nil + before_action { response.headers['Vary'] = 'Accept' } 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 0d897e8e2..2b296ea3b 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 # rubocop:disable Rails/ApplicationController + class WebfingerController < ActionController::Base include RoutingHelper before_action :set_account @@ -18,14 +18,7 @@ module WellKnown private def set_account - username = username_from_resource - @account = begin - if username == Rails.configuration.x.local_domain - Account.representative - else - Account.find_local!(username) - end - end + @account = Account.find_local!(username_from_resource) end def username_from_resource @@ -41,12 +34,7 @@ module WellKnown end def check_account_suspension - gone if @account.suspended_permanently? - end - - def gone - expires_in(3.minutes, public: true) - head 410 + expires_in(3.minutes, public: true) && gone if @account.suspended_permanently? end def bad_request @@ -58,5 +46,9 @@ 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 b8277ee17..e15aee6df 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&.settings&.[]('hide_followers_count') + Setting.hide_followers_count || account.user&.setting_hide_followers_count end def account_description(account) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3148756b7..d2bf79790 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -32,6 +32,10 @@ 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 @@ -52,7 +56,7 @@ module ApplicationHelper if closed_registrations? || omniauth_only? 'https://joinmastodon.org/#getting-started' else - ENV.fetch('SSO_ACCOUNT_SIGN_UP', new_user_registration_path) + new_user_registration_path end end @@ -113,10 +117,6 @@ module ApplicationHelper 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')) @@ -143,22 +143,34 @@ 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) + 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)) }) end def body_classes - output = body_class_string.split + output = (@body_classes || '').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.compact_blank.join(' ') + output.reject(&:blank?).join(' ') end def cdn_host @@ -170,11 +182,11 @@ module ApplicationHelper end def storage_host - "https://#{storage_host_var}" + "https://#{ENV['S3_ALIAS_HOST'].presence || ENV['S3_CLOUDFRONT_HOST']}" end def storage_host? - storage_host_var.present? + ENV['S3_ALIAS_HOST'].present? || ENV['S3_CLOUDFRONT_HOST'].present? end def quote_wrap(text, line_width: 80, break_sequence: "\n") @@ -232,10 +244,4 @@ 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 2b9c233c2..548c95411 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 diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 2f5fecaae..69d3be752 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -24,6 +24,7 @@ module ContextHelper voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, + quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, }.freeze def full_context diff --git a/app/helpers/formatting_helper.rb b/app/helpers/formatting_helper.rb index 5b2ac1a2a..8c9089d02 100644 --- a/app/helpers/formatting_helper.rb +++ b/app/helpers/formatting_helper.rb @@ -15,7 +15,17 @@ module FormattingHelper module_function :extract_status_plain_text def status_content_format(status) - html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type) + base = html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type) + + if status.quote? && status.local? + after_html = begin + "#{status.quote.to_log_permalink}" + end.html_safe # rubocop:disable Rails/OutputSafety + + base + after_html + else + base + end end def rss_status_content_format(status) diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index c5b83326d..ea2196086 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 account--minimal') do + content_tag(:div, class: 'account') do content_tag(:div, class: 'account__wrapper') do section = if account.nil? content_tag(:div, class: 'account__display-name') do diff --git a/app/helpers/instance_helper.rb b/app/helpers/instance_helper.rb index 893afdd51..bedfe6f30 100644 --- a/app/helpers/instance_helper.rb +++ b/app/helpers/instance_helper.rb @@ -9,17 +9,13 @@ module InstanceHelper @site_hostname ||= Addressable::URI.parse("//#{Rails.configuration.x.local_domain}").display_uri.host end - def description_for_sign_up(invite = nil) - safe_join([description_prefix(invite), I18n.t('auth.description.suffix')], ' ') - end + def description_for_sign_up + prefix = 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 - 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 + safe_join([prefix, I18n.t('auth.description.suffix')], ' ') end end diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index ce3ff094f..24362b61e 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -63,11 +63,11 @@ module JsonLdHelper uri.nil? || !uri.start_with?('http://', 'https://') end - def non_matching_uri_hosts?(base_url, comparison_url) - return true if unsupported_uri_scheme?(comparison_url) + def invalid_origin?(url) + return true if unsupported_uri_scheme?(url) - needle = Addressable::URI.parse(comparison_url).host - haystack = Addressable::URI.parse(base_url).host + needle = Addressable::URI.parse(url).host + haystack = Addressable::URI.parse(@account.uri).host !haystack.casecmp(needle).zero? end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 840a18d3e..bbf0a97fc 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ModuleLength + module LanguagesHelper ISO_639_1 = { aa: ['Afar', 'Afaraf'].freeze, @@ -199,6 +201,7 @@ module LanguagesHelper sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze, szl: ['Silesian', 'ślůnsko godka'].freeze, + tai: ['Tai', 'ภาษาไท or ภาษาไต'].freeze, tok: ['Toki Pona', 'toki pona'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index ae89cec78..3d5592867 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,6 +5,10 @@ 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 diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index f1f1ea872..d1e3fddaf 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)].compact_blank.join(' · ')] + components = [[media_summary(status), status_text_summary(status)].reject(&:blank?).join(' · ')] if status.spoiler_text.blank? components << status.text components << poll_summary(status) end - components.compact_blank.join("\n\n") + components.reject(&:blank?).join("\n\n") end def stream_link_target @@ -105,10 +105,94 @@ 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 97b2f4e30..ac1b2f95f 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -2,7 +2,6 @@ import 'packs/public-path'; import { delegate } from '@rails/ujs'; - import ready from '../mastodon/ready'; const setAnnouncementEndsAttributes = (target) => { diff --git a/app/javascript/core/mailer.js b/app/javascript/core/mailer.js index a2ad5e73a..a4b6d5446 100644 --- a/app/javascript/core/mailer.js +++ b/app/javascript/core/mailer.js @@ -1,3 +1,3 @@ -import '../styles/mailer.scss'; +require('../styles/mailer.scss'); require.context('../icons'); diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js index 01b4157f8..5c7a51f44 100644 --- a/app/javascript/core/public.js +++ b/app/javascript/core/public.js @@ -1,8 +1,10 @@ // This file will be loaded on public pages, regardless of theme. import 'packs/public-path'; +import ready from '../mastodon/ready'; -import { delegate } from '@rails/ujs'; +const { delegate } = require('@rails/ujs'); +const { length } = require('stringz'); 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 40537377c..d578463a3 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -1,9 +1,9 @@ // 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'; diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index 30676dcf5..b9144e43a 100644 --- a/app/javascript/core/theme.yml +++ b/app/javascript/core/theme.yml @@ -16,5 +16,4 @@ 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 e76700a48..f076cdf30 100644 --- a/app/javascript/core/two_factor_authentication.js +++ b/app/javascript/core/two_factor_authentication.js @@ -1,8 +1,6 @@ import 'packs/public-path'; - -import * as WebAuthnJSON from '@github/webauthn-json'; import axios from 'axios'; - +import * as WebAuthnJSON from '@github/webauthn-json'; import ready from '../mastodon/ready'; import 'regenerator-runtime/runtime'; diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 8c57406d3..6b5b2ade5 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -1,6 +1,5 @@ import api, { getLinks } from '../api'; - -import { importFetchedAccount, importFetchedAccounts } from './importer'; +import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; @@ -82,15 +81,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_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_READY = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY'; export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR'; export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE'; -export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_FETCH_FAIL = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_FETCH_FAIL'; - export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET'; @@ -847,8 +841,6 @@ export function fetchPinnedAccountsFail(error) { export function fetchPinnedAccountsSuggestions(q) { return (dispatch, getState) => { - dispatch(fetchPinnedAccountsSuggestionsRequest()); - const params = { q, resolve: false, @@ -858,32 +850,19 @@ export function fetchPinnedAccountsSuggestions(q) { api(getState).get('/api/v1/accounts/search', { params }).then(response => { dispatch(importFetchedAccounts(response.data)); - dispatch(fetchPinnedAccountsSuggestionsSuccess(q, response.data)); - }).catch(err => dispatch(fetchPinnedAccountsSuggestionsFail(err))); + dispatch(fetchPinnedAccountsSuggestionsReady(q, response.data)); + }); }; } -export function fetchPinnedAccountsSuggestionsRequest() { +export function fetchPinnedAccountsSuggestionsReady(query, accounts) { return { - type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_REQUEST, - }; -} - -export function fetchPinnedAccountsSuggestionsSuccess(query, accounts) { - return { - type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_SUCCESS, + type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY, query, accounts, }; } -export function fetchPinnedAccountsSuggestionsFail(error) { - return { - type: PINNED_ACCOUNTS_SUGGESTIONS_FETCH_FAIL, - error, - }; -} - export function clearPinnedAccountsSuggestions() { return { type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR, diff --git a/app/javascript/flavours/glitch/actions/announcements.js b/app/javascript/flavours/glitch/actions/announcements.js index 339c5f3ad..586dcfd33 100644 --- a/app/javascript/flavours/glitch/actions/announcements.js +++ b/app/javascript/flavours/glitch/actions/announcements.js @@ -1,5 +1,4 @@ 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 new file mode 100644 index 000000000..de2d93e29 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/app.js @@ -0,0 +1,6 @@ +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/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js index e293657ad..192aa3ce4 100644 --- a/app/javascript/flavours/glitch/actions/blocks.js +++ b/app/javascript/flavours/glitch/actions/blocks.js @@ -1,5 +1,4 @@ import api, { getLinks } from '../api'; - import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; import { openModal } from './modal'; @@ -95,6 +94,6 @@ export function initBlockModal(account) { account, }); - dispatch(openModal({ modalType: 'BLOCK' })); + dispatch(openModal('BLOCK')); }; } diff --git a/app/javascript/flavours/glitch/actions/bookmarks.js b/app/javascript/flavours/glitch/actions/bookmarks.js index 0b16f61e6..3c8eec546 100644 --- a/app/javascript/flavours/glitch/actions/bookmarks.js +++ b/app/javascript/flavours/glitch/actions/bookmarks.js @@ -1,5 +1,4 @@ import api, { getLinks } from '../api'; - import { importFetchedStatuses } from './importer'; export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/boosts.js b/app/javascript/flavours/glitch/actions/boosts.js index 1fc2e391e..c0f0f3acc 100644 --- a/app/javascript/flavours/glitch/actions/boosts.js +++ b/app/javascript/flavours/glitch/actions/boosts.js @@ -14,10 +14,7 @@ export function initBoostModal(props) { privacy, }); - dispatch(openModal({ - modalType: 'BOOST', - modalProps: props, - })); + dispatch(openModal('BOOST', props)); }; } diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index e747f8a01..9e8b2bcfb 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -1,13 +1,11 @@ -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'; @@ -84,6 +82,9 @@ export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS'; export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS'; +export const COMPOSE_QUOTE = 'COMPOSE_QUOTE'; +export const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL'; + const messages = defineMessages({ uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, @@ -137,6 +138,25 @@ export function cancelReplyCompose() { }; } +export function quoteCompose(status, router) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_QUOTE, + status: status, + }); + + if (!getState().getIn(['compose', 'mounted'])) { + router.push('/publish'); + } + }; +}; + +export function cancelQuoteCompose() { + return { + type: COMPOSE_QUOTE_CANCEL, + }; +}; + export function resetCompose() { return { type: COMPOSE_RESET, @@ -210,6 +230,7 @@ export function submitCompose(routerHistory) { status, content_type: getState().getIn(['compose', 'content_type']), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), + quote_id: getState().getIn(['compose', 'quote_id'], null), media_ids: media.map(item => item.get('id')), media_attributes, sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), @@ -316,42 +337,46 @@ export function uploadCompose(files) { dispatch(uploadComposeRequest()); - for (const [i, file] of Array.from(files).entries()) { + for (const [i, f] of Array.from(files).entries()) { if (media.size + i > 3) break; - const data = new FormData(); - data.append('file', file); + resizeImage(f).then(file => { + const data = new FormData(); + data.append('file', file); + // Account for disparity in size of original image and resized data + total += file.size - f.size; - api(getState).post('/api/v2/media', data, { - onUploadProgress: function({ loaded }){ - progress[i] = loaded; - dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); - }, - }).then(({ status, data }) => { - // If server-side processing of the media attachment has not completed yet, - // poll the server until it is, before showing the media attachment as uploaded + return api(getState).post('/api/v2/media', data, { + onUploadProgress: function({ loaded }){ + progress[i] = loaded; + dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total)); + }, + }).then(({ status, data }) => { + // If server-side processing of the media attachment has not completed yet, + // poll the server until it is, before showing the media attachment as uploaded - if (status === 200) { - dispatch(uploadComposeSuccess(data, file)); - } else if (status === 202) { - dispatch(uploadComposeProcessing()); + if (status === 200) { + dispatch(uploadComposeSuccess(data, f)); + } else if (status === 202) { + dispatch(uploadComposeProcessing()); - let tryCount = 1; + let tryCount = 1; - const poll = () => { - api(getState).get(`/api/v1/media/${data.id}`).then(response => { - if (response.status === 200) { - dispatch(uploadComposeSuccess(response.data, file)); - } else if (response.status === 206) { - const retryAfter = (Math.log2(tryCount) || 1) * 1000; - tryCount += 1; - setTimeout(() => poll(), retryAfter); - } - }).catch(error => dispatch(uploadComposeFail(error))); - }; + const poll = () => { + api(getState).get(`/api/v1/media/${data.id}`).then(response => { + if (response.status === 200) { + dispatch(uploadComposeSuccess(response.data, f)); + } else if (response.status === 206) { + const retryAfter = (Math.log2(tryCount) || 1) * 1000; + tryCount += 1; + setTimeout(() => poll(), retryAfter); + } + }).catch(error => dispatch(uploadComposeFail(error))); + }; - poll(); - } + poll(); + } + }); }).catch(error => dispatch(uploadComposeFail(error))); } }; @@ -411,10 +436,7 @@ export function initMediaEditModal(id) { id, }); - dispatch(openModal({ - modalType: 'FOCAL_POINT', - modalProps: { id }, - })); + dispatch(openModal('FOCAL_POINT', { id })); }; } @@ -442,12 +464,16 @@ 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')) { - const { focus, ...other } = params; - const data = { ...media.toJS(), ...other }; + let { description, focus } = params; + const data = media.toJS(); + + if (description) { + data.description = description; + } if (focus) { - const [x, y] = focus.split(','); - data.meta = { focus: { x: parseFloat(x), y: parseFloat(y) } }; + focus = focus.split(','); + data.meta = { focus: { x: parseFloat(focus[0]), y: parseFloat(focus[1]) } }; } dispatch(changeUploadComposeSuccess(data, true)); diff --git a/app/javascript/flavours/glitch/actions/conversations.js b/app/javascript/flavours/glitch/actions/conversations.js index 8c4c4529f..4ef654b1f 100644 --- a/app/javascript/flavours/glitch/actions/conversations.js +++ b/app/javascript/flavours/glitch/actions/conversations.js @@ -1,5 +1,4 @@ import api, { getLinks } from '../api'; - import { importFetchedAccounts, importFetchedStatuses, diff --git a/app/javascript/flavours/glitch/actions/directory.js b/app/javascript/flavours/glitch/actions/directory.js index cda63f2b5..4b2b6dd56 100644 --- a/app/javascript/flavours/glitch/actions/directory.js +++ b/app/javascript/flavours/glitch/actions/directory.js @@ -1,7 +1,6 @@ import api from '../api'; - -import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; +import { fetchRelationships } from './accounts'; export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; diff --git a/app/javascript/flavours/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js index 2d4d4e620..7388e0c58 100644 --- a/app/javascript/flavours/glitch/actions/favourites.js +++ b/app/javascript/flavours/glitch/actions/favourites.js @@ -1,5 +1,4 @@ import api, { getLinks } from '../api'; - import { importFetchedStatuses } from './importer'; export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/filters.js b/app/javascript/flavours/glitch/actions/filters.js index a11956ac5..e9c609fc8 100644 --- a/app/javascript/flavours/glitch/actions/filters.js +++ b/app/javascript/flavours/glitch/actions/filters.js @@ -1,5 +1,4 @@ import api from '../api'; - import { openModal } from './modal'; export const FILTERS_FETCH_REQUEST = 'FILTERS_FETCH_REQUEST'; @@ -15,12 +14,9 @@ export const FILTERS_CREATE_SUCCESS = 'FILTERS_CREATE_SUCCESS'; export const FILTERS_CREATE_FAIL = 'FILTERS_CREATE_FAIL'; export const initAddFilter = (status, { contextType }) => dispatch => - dispatch(openModal({ - modalType: 'FILTER', - modalProps: { - statusId: status?.get('id'), - contextType: contextType, - }, + dispatch(openModal('FILTER', { + statusId: status?.get('id'), + contextType: contextType, })); export const fetchFilters = () => (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/history.js b/app/javascript/flavours/glitch/actions/history.js index 52401b7dc..c142aaf61 100644 --- a/app/javascript/flavours/glitch/actions/history.js +++ b/app/javascript/flavours/glitch/actions/history.js @@ -1,5 +1,4 @@ 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 540e6cba7..567db7eeb 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -1,12 +1,11 @@ import escapeTextContentForBrowser from 'escape-html'; - import emojify from 'flavours/glitch/features/emoji/emoji'; -import { autoHideCW } from 'flavours/glitch/utils/content_warning'; import { unescapeHTML } from 'flavours/glitch/utils/html'; +import { autoHideCW } from 'flavours/glitch/utils/content_warning'; const domParser = new DOMParser(); -const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { +const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { obj[`:${emoji.shortcode}:`] = emoji; return obj; }, {}); @@ -20,7 +19,7 @@ export function searchTextFromRawStatus (status) { export function normalizeAccount(account) { account = { ...account }; - const emojiMap = makeEmojiMap(account.emojis); + const emojiMap = makeEmojiMap(account); const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); @@ -75,62 +74,67 @@ export function normalizeStatus(status, normalOldStatus, settings) { normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); normalStatus.hidden = normalOldStatus.get('hidden'); + normalStatus.quote = normalOldStatus.get('quote'); + normalStatus.quote_hidden = normalOldStatus.get('quote_hidden'); } 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.emojis); + const emojiMap = makeEmojiMap(normalStatus); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText); + + if (status.quote && status.quote.id) { + const quote_spoilerText = status.quote.spoiler_text || ''; + const quote_searchContent = [quote_spoilerText, status.quote.content].join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); + + const quote_emojiMap = makeEmojiMap(normalStatus.quote); + + const quote_account_emojiMap = makeEmojiMap(status.quote.account); + const displayName = normalStatus.quote.account.display_name.length === 0 ? normalStatus.quote.account.username : normalStatus.quote.account.display_name; + normalStatus.quote.account.display_name_html = emojify(escapeTextContentForBrowser(displayName), quote_account_emojiMap); + normalStatus.quote.search_index = domParser.parseFromString(quote_searchContent, 'text/html').documentElement.textContent; + let docElem = domParser.parseFromString(normalStatus.quote.content, 'text/html').documentElement; + Array.from(docElem.querySelectorAll('span.quote-inline'), span => span.remove()); + Array.from(docElem.querySelectorAll('p,br'), line => { + let parentNode = line.parentNode; + if (line.nextSibling) { + parentNode.insertBefore(document.createTextNode(' '), line.nextSibling); + } + }); + let _contentHtml = docElem.textContent; + normalStatus.quote.contentHtml = '

'+emojify(_contentHtml.substr(0, 150), quote_emojiMap) + (_contentHtml.substr(150) ? '...' : '')+'

'; + normalStatus.quote.spoilerHtml = emojify(escapeTextContentForBrowser(quote_spoilerText), quote_emojiMap); + normalStatus.quote_hidden = (quote_spoilerText.length > 0 || normalStatus.quote.sensitive) && autoHideCW(settings, quote_spoilerText); + + // delete the quote link!!!! + let parentDocElem = domParser.parseFromString(normalStatus.contentHtml, 'text/html').documentElement; + Array.from(parentDocElem.querySelectorAll('span.quote-inline'), span => span.remove()); + normalStatus.contentHtml = parentDocElem.children[1].innerHTML; + } } 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(poll.emojis); + const emojiMap = makeEmojiMap(normalPoll); normalPoll.options = poll.options.map((option, index) => ({ ...option, voted: poll.own_votes && poll.own_votes.includes(index), - titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), + title_emojified: 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.emojis); + const emojiMap = makeEmojiMap(normalAnnouncement); 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 90cd3c6d3..8f515c990 100644 --- a/app/javascript/flavours/glitch/actions/interactions.js +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -1,5 +1,4 @@ import api from '../api'; - import { importFetchedAccounts, importFetchedStatus } from './importer'; export const REBLOG_REQUEST = 'REBLOG_REQUEST'; diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index b0789cd42..5ab922436 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -1,7 +1,6 @@ import api from '../api'; - -import { showAlertForError } from './alerts'; import { importFetchedAccounts } from './importer'; +import { showAlertForError } from './alerts'; export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; @@ -151,10 +150,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).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 f2878daa5..adf7fd2ab 100644 --- a/app/javascript/flavours/glitch/actions/local_settings.js +++ b/app/javascript/flavours/glitch/actions/local_settings.js @@ -1,5 +1,4 @@ import { expandSpoilers, disableSwiping } from 'flavours/glitch/initial_state'; - import { openModal } from './modal'; export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; @@ -28,12 +27,9 @@ export function checkDeprecatedLocalSettings() { } if (changed_settings.length > 0) { - dispatch(openModal({ - modalType: 'DEPRECATED_SETTINGS', - modalProps: { - settings: changed_settings, - onConfirm: () => dispatch(clearDeprecatedLocalSettings()), - }, + dispatch(openModal('DEPRECATED_SETTINGS', { + settings: changed_settings, + onConfirm: () => dispatch(clearDeprecatedLocalSettings()), })); } }; diff --git a/app/javascript/flavours/glitch/actions/markers.js b/app/javascript/flavours/glitch/actions/markers.js index ccb1b23d6..dfd701cbb 100644 --- a/app/javascript/flavours/glitch/actions/markers.js +++ b/app/javascript/flavours/glitch/actions/markers.js @@ -1,9 +1,7 @@ -import { List as ImmutableList } from 'immutable'; - -import { debounce } from 'lodash'; - import api from '../api'; -import { compareId } from '../compare_id'; +import { debounce } from 'lodash'; +import compareId from '../compare_id'; +import { List as ImmutableList } from 'immutable'; export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; @@ -57,7 +55,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.send(JSON.stringify(params)); + client.SUBMIT(JSON.stringify(params)); } catch (e) { // Do not make the BeforeUnload handler error out } diff --git a/app/javascript/flavours/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js new file mode 100644 index 000000000..ef2ae0e4c --- /dev/null +++ b/app/javascript/flavours/glitch/actions/modal.js @@ -0,0 +1,18 @@ +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/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js index 4af927d93..aa47d1464 100644 --- a/app/javascript/flavours/glitch/actions/mutes.js +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -1,9 +1,7 @@ -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'; @@ -98,7 +96,7 @@ export function initMuteModal(account) { account, }); - dispatch(openModal({ modalType: 'MUTE' })); + dispatch(openModal('MUTE')); }; } diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index f370905de..ecdac6256 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -1,15 +1,5 @@ -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, @@ -19,9 +9,12 @@ 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'; diff --git a/app/javascript/flavours/glitch/actions/onboarding.js b/app/javascript/flavours/glitch/actions/onboarding.js index a4a525c42..5038b7eb6 100644 --- a/app/javascript/flavours/glitch/actions/onboarding.js +++ b/app/javascript/flavours/glitch/actions/onboarding.js @@ -6,9 +6,7 @@ export function showOnboardingOnce() { const alreadySeen = getState().getIn(['settings', 'onboarded']); if (!alreadySeen) { - dispatch(openModal({ - modalType: 'ONBOARDING', - })); + dispatch(openModal('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 898375abe..33d8d57d4 100644 --- a/app/javascript/flavours/glitch/actions/picture_in_picture.js +++ b/app/javascript/flavours/glitch/actions/picture_in_picture.js @@ -20,10 +20,9 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE'; * @param {string} accountId * @param {string} playerType * @param {MediaProps} props - * @returns {object} + * @return {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 8aca199e9..d8c0a1373 100644 --- a/app/javascript/flavours/glitch/actions/pin_statuses.js +++ b/app/javascript/flavours/glitch/actions/pin_statuses.js @@ -1,14 +1,12 @@ -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()); diff --git a/app/javascript/flavours/glitch/actions/polls.js b/app/javascript/flavours/glitch/actions/polls.js index a37410dc9..8e8b82df5 100644 --- a/app/javascript/flavours/glitch/actions/polls.js +++ b/app/javascript/flavours/glitch/actions/polls.js @@ -1,5 +1,4 @@ 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 46b63867f..9dcc4bd4b 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 { saveSettings } from './registerer'; import { setAlerts } from './setter'; +import { saveSettings } from './registerer'; 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 336bbc686..bc5634233 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js @@ -1,6 +1,5 @@ import api from '../../api'; import { pushNotificationsSetting } from '../../settings'; - import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; // Taken from https://www.npmjs.com/package/web-push diff --git a/app/javascript/flavours/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js index 756b8cd05..fbe5b3791 100644 --- a/app/javascript/flavours/glitch/actions/reports.js +++ b/app/javascript/flavours/glitch/actions/reports.js @@ -1,5 +1,4 @@ import api from '../api'; - import { openModal } from './modal'; export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; @@ -7,12 +6,9 @@ export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; export const initReport = (account, status) => dispatch => - dispatch(openModal({ - modalType: 'REPORT', - modalProps: { - accountId: account.get('id'), - statusId: status?.get('id'), - }, + dispatch(openModal('REPORT', { + 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 d5154c6a8..0012808e5 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -1,5 +1,4 @@ import api from '../api'; - import { fetchRelationships } from './accounts'; import { importFetchedAccounts, importFetchedStatuses } from './importer'; diff --git a/app/javascript/flavours/glitch/actions/server.js b/app/javascript/flavours/glitch/actions/server.js index bd784906d..31d4aea10 100644 --- a/app/javascript/flavours/glitch/actions/server.js +++ b/app/javascript/flavours/glitch/actions/server.js @@ -1,15 +1,10 @@ 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'; @@ -42,29 +37,6 @@ 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) => { dispatch(fetchExtendedDescriptionRequest()); diff --git a/app/javascript/flavours/glitch/actions/settings.js b/app/javascript/flavours/glitch/actions/settings.js index 120ae133e..60f0abf95 100644 --- a/app/javascript/flavours/glitch/actions/settings.js +++ b/app/javascript/flavours/glitch/actions/settings.js @@ -1,7 +1,5 @@ -import { debounce } from 'lodash'; - import api from '../api'; - +import { debounce } from 'lodash'; import { showAlertForError } from './alerts'; export const SETTING_CHANGE = 'SETTING_CHANGE'; diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 5bdd31c34..487cd6988 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 { ensureComposeIsVisible, setComposeToStatus } from './compose'; -import { importFetchedStatus, importFetchedStatuses } from './importer'; import { deleteFromTimelines } from './timelines'; +import { importFetchedStatus, importFetchedStatuses } from './importer'; +import { ensureComposeIsVisible, setComposeToStatus } from './compose'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -344,8 +344,7 @@ export const translateStatusFail = (id, error) => ({ error, }); -export const undoStatusTranslation = (id, pollId) => ({ +export const undoStatusTranslation = id => ({ 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 e57b37a12..137b68e22 100644 --- a/app/javascript/flavours/glitch/actions/store.js +++ b/app/javascript/flavours/glitch/actions/store.js @@ -1,5 +1,4 @@ import { Iterable, fromJS } from 'immutable'; - import { hydrateCompose } from './compose'; import { importFetchedAccounts } from './importer'; import { saveSettings } from './settings'; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index f1c44d2e2..ffac1b258 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -1,18 +1,6 @@ // @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, @@ -24,10 +12,22 @@ 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 - * @returns {number} + * @return {number} */ const randomUpTo = max => Math.floor(Math.random() * Math.floor(max)); @@ -40,24 +40,19 @@ const randomUpTo = max => * @param {function(Function, Function): void} [options.fallback] * @param {function(): void} [options.fillGaps] * @param {function(object): boolean} [options.accept] - * @returns {function(): void} + * @return {function(): void} */ -export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => { - const { messages } = getLocale(); - - return connectStream(channelName, params, (dispatch, getState) => { +export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => + 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)); }); }; @@ -66,7 +61,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti onConnect() { dispatch(connectTimeline(timelineId)); - // @ts-expect-error if (pollingId) { clearTimeout(pollingId); pollingId = null; @@ -81,7 +75,6 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti dispatch(disconnectTimeline(timelineId)); if (options.fallback) { - // @ts-expect-error pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000)); } }, @@ -89,30 +82,24 @@ 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': @@ -122,31 +109,27 @@ 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)))))); }; /** - * @returns {function(): void} + * @return {function(): void} */ export const connectUserStream = () => - // @ts-expect-error connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); /** * @param {Object} options * @param {boolean} [options.onlyMedia] - * @returns {function(): void} + * @return {function(): void} */ export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) }); @@ -156,7 +139,7 @@ export const connectCommunityStream = ({ onlyMedia } = {}) => * @param {boolean} [options.onlyMedia] * @param {boolean} [options.onlyRemote] * @param {boolean} [options.allowLocalOnly] - * @returns {function(): void} + * @return {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 }) }); @@ -166,20 +149,20 @@ export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = * @param {string} tagName * @param {boolean} onlyLocal * @param {function(object): boolean} accept - * @returns {function(): void} + * @return {function(): void} */ export const connectHashtagStream = (columnId, tagName, onlyLocal, accept) => connectTimelineStream(`hashtag:${columnId}${onlyLocal ? ':local' : ''}`, `hashtag${onlyLocal ? ':local' : ''}`, { tag: tagName }, { accept }); /** - * @returns {function(): void} + * @return {function(): void} */ export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); /** * @param {string} listId - * @returns {function(): void} + * @return {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 870a31102..9e8cd1ea4 100644 --- a/app/javascript/flavours/glitch/actions/suggestions.js +++ b/app/javascript/flavours/glitch/actions/suggestions.js @@ -1,7 +1,6 @@ import api from '../api'; - -import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; +import { fetchRelationships } from './accounts'; export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST'; export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS'; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 7d4d56a78..eb817daf9 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,12 +1,10 @@ -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'; @@ -123,6 +121,7 @@ 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)); @@ -164,10 +163,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); }; diff --git a/app/javascript/flavours/glitch/actions/trends.js b/app/javascript/flavours/glitch/actions/trends.js index d31442388..edda0b5b5 100644 --- a/app/javascript/flavours/glitch/actions/trends.js +++ b/app/javascript/flavours/glitch/actions/trends.js @@ -1,5 +1,4 @@ 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 948ffbc95..6bbddbef6 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'; + /** * @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 new file mode 100644 index 000000000..12096d902 --- /dev/null +++ b/app/javascript/flavours/glitch/base_polyfills.js @@ -0,0 +1,47 @@ +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/flavours/glitch/blurhash.js b/app/javascript/flavours/glitch/blurhash.js new file mode 100644 index 000000000..5adcc3e77 --- /dev/null +++ b/app/javascript/flavours/glitch/blurhash.js @@ -0,0 +1,112 @@ +const DIGIT_CHARACTERS = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + '#', + '$', + '%', + '*', + '+', + ',', + '-', + '.', + ':', + ';', + '=', + '?', + '@', + '[', + ']', + '^', + '_', + '{', + '|', + '}', + '~', +]; + +export const decode83 = (str) => { + let value = 0; + let c, digit; + + for (let i = 0; i < str.length; i++) { + c = str[i]; + digit = DIGIT_CHARACTERS.indexOf(c); + value = value * 83 + digit; + } + + return value; +}; + +export const intToRGB = int => ({ + r: Math.max(0, (int >> 16)), + g: Math.max(0, (int >> 8) & 255), + b: Math.max(0, (int & 255)), +}); + +export const getAverageFromBlurhash = blurhash => { + if (!blurhash) { + return null; + } + + return intToRGB(decode83(blurhash.slice(2, 6))); +}; diff --git a/app/javascript/flavours/glitch/compare_id.js b/app/javascript/flavours/glitch/compare_id.js new file mode 100644 index 000000000..d2bd74f44 --- /dev/null +++ b/app/javascript/flavours/glitch/compare_id.js @@ -0,0 +1,11 @@ +export default function compareId (id1, id2) { + if (id1 === id2) { + return 0; + } + + if (id1.length === id2.length) { + return id1 > id2 ? 1 : -1; + } else { + return id1.length > id2.length ? 1 : -1; + } +} diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx index 911b4d55e..7ce4b65aa 100644 --- a/app/javascript/flavours/glitch/components/account.jsx +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -1,30 +1,20 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import { defineMessages, injectIntl } from 'react-intl'; - -import classNames from 'classnames'; - +import React, { Fragment } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import { counterRenderer } from 'flavours/glitch/components/common_counter'; -import { Icon } from 'flavours/glitch/components/icon'; -import ShortNumber from 'flavours/glitch/components/short_number'; -import { Skeleton } from 'flavours/glitch/components/skeleton'; -import { me } from 'flavours/glitch/initial_state'; - -import { Avatar } from './avatar'; -import { DisplayName } from './display_name'; -import { IconButton } from './icon_button'; +import PropTypes from 'prop-types'; +import Avatar from './avatar'; +import DisplayName from './display_name'; import Permalink from './permalink'; -import { RelativeTimestamp } from './relative_timestamp'; - +import IconButton from './icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { me } from 'flavours/glitch/initial_state'; +import RelativeTimestamp from './relative_timestamp'; +import Skeleton from 'flavours/glitch/components/skeleton'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, 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}' }, @@ -33,26 +23,7 @@ const messages = defineMessages({ block: { id: 'account.block', defaultMessage: 'Block @{name}' }, }); -class VerifiedBadge extends React.PureComponent { - - static propTypes = { - link: PropTypes.string.isRequired, - verifiedAt: PropTypes.string.isRequired, - }; - - render () { - const { link } = this.props; - - return ( - - - - - ); - } - -} - +export default @injectIntl class Account extends ImmutablePureComponent { static propTypes = { @@ -64,7 +35,6 @@ class Account extends ImmutablePureComponent { onMuteNotifications: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, hidden: PropTypes.bool, - minimal: PropTypes.bool, small: PropTypes.bool, actionIcon: PropTypes.string, actionTitle: PropTypes.string, @@ -111,20 +81,15 @@ class Account extends ImmutablePureComponent { actionTitle, defaultAction, size, - minimal, } = this.props; if (!account) { return ( -
+
-
- -
- - -
+
+
@@ -133,10 +98,10 @@ class Account extends ImmutablePureComponent { if (hidden) { return ( - <> + {account.get('display_name')} {account.get('username')} - + ); } @@ -164,10 +129,10 @@ class Account extends ImmutablePureComponent { hidingNotificationsButton = ; } buttons = ( - <> + {hidingNotificationsButton} - + ); } else if (defaultAction === 'mute') { buttons = ; @@ -178,18 +143,9 @@ class Account extends ImmutablePureComponent { } } - let muteTimeRemaining; - + let mute_expires_at; if (account.get('mute_expires_at')) { - muteTimeRemaining = <>· ; - } - - let verification; - - const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at')); - - if (firstVerifiedField) { - verification = <>· ; + mute_expires_at =
; } return small ? ( @@ -210,24 +166,17 @@ class Account extends ImmutablePureComponent { /> ) : ( -
+
-
- -
- -
- - {!minimal && <> {verification} {muteTimeRemaining}} -
+
+ {mute_expires_at} +
{buttons ? - !minimal && ( -
- {buttons} -
- ) +
+ {buttons} +
: null}
@@ -235,5 +184,3 @@ class Account extends ImmutablePureComponent { } } - -export default injectIntl(Account); diff --git a/app/javascript/flavours/glitch/components/admin/Counter.jsx b/app/javascript/flavours/glitch/components/admin/Counter.jsx index 9bb792fc9..5b6a19f8d 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.jsx +++ b/app/javascript/flavours/glitch/components/admin/Counter.jsx @@ -1,14 +1,10 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedNumber } from 'react-intl'; - -import classNames from 'classnames'; - -import { Sparklines, SparklinesCurve } from 'react-sparklines'; - import api from 'flavours/glitch/api'; -import { Skeleton } from 'flavours/glitch/components/skeleton'; +import { FormattedNumber } from 'react-intl'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; +import classNames from 'classnames'; +import Skeleton from 'flavours/glitch/components/skeleton'; const percIncrease = (a, b) => { let percent; @@ -28,7 +24,7 @@ const percIncrease = (a, b) => { return percent; }; -export default class Counter extends PureComponent { +export default class Counter extends React.PureComponent { static propTypes = { measure: PropTypes.string.isRequired, @@ -66,25 +62,25 @@ export default class Counter extends 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}
@@ -100,7 +96,7 @@ export default class Counter extends PureComponent { )}
- + ); if (href) { diff --git a/app/javascript/flavours/glitch/components/admin/Dimension.jsx b/app/javascript/flavours/glitch/components/admin/Dimension.jsx index 793fe2dd7..3dac8c6c2 100644 --- a/app/javascript/flavours/glitch/components/admin/Dimension.jsx +++ b/app/javascript/flavours/glitch/components/admin/Dimension.jsx @@ -1,13 +1,11 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedNumber } from 'react-intl'; - import api from 'flavours/glitch/api'; -import { Skeleton } from 'flavours/glitch/components/skeleton'; +import { FormattedNumber } from 'react-intl'; import { roundTo10 } from 'flavours/glitch/utils/numbers'; +import Skeleton from 'flavours/glitch/components/skeleton'; -export default class Dimension extends PureComponent { +export default class Dimension extends React.PureComponent { static propTypes = { dimension: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx index d72465e4a..771dbb452 100644 --- a/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx +++ b/app/javascript/flavours/glitch/components/admin/ReportReasonSelector.jsx @@ -1,11 +1,8 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { injectIntl, defineMessages } from 'react-intl'; - -import classNames from 'classnames'; - import api from 'flavours/glitch/api'; +import { injectIntl, defineMessages } from 'react-intl'; +import classNames from 'classnames'; const messages = defineMessages({ other: { id: 'report.categories.other', defaultMessage: 'Other' }, @@ -13,7 +10,7 @@ const messages = defineMessages({ violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' }, }); -class Category extends PureComponent { +class Category extends React.PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -36,7 +33,7 @@ class Category extends PureComponent { const { id, text, disabled, selected, children } = this.props; return ( -
+
{selected && }
@@ -55,7 +52,7 @@ class Category extends PureComponent { } -class Rule extends PureComponent { +class Rule extends React.PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -77,7 +74,7 @@ class Rule extends PureComponent { const { id, text, disabled, selected } = this.props; return ( -
+
{selected && } {text} @@ -87,7 +84,8 @@ class Rule extends PureComponent { } -class ReportReasonSelector extends PureComponent { +export default @injectIntl +class ReportReasonSelector extends React.PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -159,5 +157,3 @@ class ReportReasonSelector extends PureComponent { } } - -export default injectIntl(ReportReasonSelector); diff --git a/app/javascript/flavours/glitch/components/admin/Retention.jsx b/app/javascript/flavours/glitch/components/admin/Retention.jsx index 2cfc30b6f..e1ba3f6c9 100644 --- a/app/javascript/flavours/glitch/components/admin/Retention.jsx +++ b/app/javascript/flavours/glitch/components/admin/Retention.jsx @@ -1,11 +1,8 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; - -import classNames from 'classnames'; - import api from 'flavours/glitch/api'; +import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl'; +import classNames from 'classnames'; import { roundTo10 } from 'flavours/glitch/utils/numbers'; const dateForCohort = cohort => { @@ -17,7 +14,7 @@ const dateForCohort = cohort => { } }; -export default class Retention extends PureComponent { +export default class Retention extends React.PureComponent { static propTypes = { start_at: PropTypes.string, diff --git a/app/javascript/flavours/glitch/components/admin/Trends.jsx b/app/javascript/flavours/glitch/components/admin/Trends.jsx index 975ea6e0f..774bf36e6 100644 --- a/app/javascript/flavours/glitch/components/admin/Trends.jsx +++ b/app/javascript/flavours/glitch/components/admin/Trends.jsx @@ -1,14 +1,11 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; - import api from 'flavours/glitch/api'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; import Hashtag from 'flavours/glitch/components/hashtag'; -export default class Trends extends PureComponent { +export default class Trends extends React.PureComponent { static propTypes = { limit: PropTypes.number.isRequired, diff --git a/app/javascript/flavours/glitch/components/animated_number.jsx b/app/javascript/flavours/glitch/components/animated_number.jsx new file mode 100644 index 000000000..dd21d97f0 --- /dev/null +++ b/app/javascript/flavours/glitch/components/animated_number.jsx @@ -0,0 +1,76 @@ +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/attachment_list.jsx b/app/javascript/flavours/glitch/components/attachment_list.jsx index 173157b0d..68b80b19f 100644 --- a/app/javascript/flavours/glitch/components/attachment_list.jsx +++ b/app/javascript/flavours/glitch/components/attachment_list.jsx @@ -1,13 +1,10 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; - +import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; import ImmutablePureComponent from 'react-immutable-pure-component'; - -import { Icon } from 'flavours/glitch/components/icon'; +import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; +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.jsx b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx index 32a996fd7..83fafbd10 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx @@ -1,10 +1,10 @@ +import React from 'react'; import PropTypes from 'prop-types'; -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 PureComponent { +export default class AutosuggestEmoji extends React.PureComponent { static propTypes = { emoji: PropTypes.object.isRequired, diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx b/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx index 37f7e20f0..d787ed07a 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.jsx @@ -1,11 +1,9 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - +import ShortNumber from 'flavours/glitch/components/short_number'; import { FormattedMessage } from 'react-intl'; -import ShortNumber from 'flavours/glitch/components/short_number'; - -export default class AutosuggestHashtag extends PureComponent { +export default class AutosuggestHashtag extends React.PureComponent { static propTypes = { tag: PropTypes.shape({ diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.jsx b/app/javascript/flavours/glitch/components/autosuggest_input.jsx index d3b7c48ab..90ff298c0 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -1,16 +1,11 @@ -import PropTypes from 'prop-types'; - -import classNames from 'classnames'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - +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'; const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { let word; @@ -159,7 +154,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { this.input.focus(); }; - UNSAFE_componentWillReceiveProps (nextProps) { + componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } @@ -185,7 +180,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { } return ( -
+
{inner}
); diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx index 86f10651d..6e6e567b9 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx @@ -1,16 +1,12 @@ -import PropTypes from 'prop-types'; - -import classNames from 'classnames'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import Textarea from 'react-textarea-autosize'; - +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'; const textAtCursorMatchesToken = (str, caretPosition) => { let word; @@ -157,7 +153,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { this.textarea.focus(); }; - UNSAFE_componentWillReceiveProps (nextProps) { + componentWillReceiveProps (nextProps) { if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) { this.setState({ suggestionsHidden: false }); } @@ -190,7 +186,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } return ( -
+
{inner}
); diff --git a/app/javascript/flavours/glitch/components/avatar.jsx b/app/javascript/flavours/glitch/components/avatar.jsx new file mode 100644 index 000000000..f30b33e70 --- /dev/null +++ b/app/javascript/flavours/glitch/components/avatar.jsx @@ -0,0 +1,79 @@ +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_composite.jsx b/app/javascript/flavours/glitch/components/avatar_composite.jsx index 5503abf4a..c0ce7761d 100644 --- a/app/javascript/flavours/glitch/components/avatar_composite.jsx +++ b/app/javascript/flavours/glitch/components/avatar_composite.jsx @@ -1,11 +1,9 @@ +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 PureComponent { +export default class AvatarComposite extends React.PureComponent { static propTypes = { accounts: ImmutablePropTypes.list.isRequired, @@ -81,7 +79,15 @@ export default class AvatarComposite extends 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.jsx b/app/javascript/flavours/glitch/components/avatar_overlay.jsx index d8215a478..01dec587a 100644 --- a/app/javascript/flavours/glitch/components/avatar_overlay.jsx +++ b/app/javascript/flavours/glitch/components/avatar_overlay.jsx @@ -1,11 +1,9 @@ +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 PureComponent { +export default class AvatarOverlay extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/components/blurhash.jsx b/app/javascript/flavours/glitch/components/blurhash.jsx new file mode 100644 index 000000000..2af5cfc56 --- /dev/null +++ b/app/javascript/flavours/glitch/components/blurhash.jsx @@ -0,0 +1,65 @@ +// @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/button.jsx b/app/javascript/flavours/glitch/components/button.jsx index bdeeeac99..40b8f5a15 100644 --- a/app/javascript/flavours/glitch/components/button.jsx +++ b/app/javascript/flavours/glitch/components/button.jsx @@ -1,9 +1,8 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - import classNames from 'classnames'; -export default class Button extends PureComponent { +export default class Button extends React.PureComponent { static propTypes = { text: PropTypes.node, diff --git a/app/javascript/flavours/glitch/components/check.jsx b/app/javascript/flavours/glitch/components/check.jsx index d818480b7..ee2ef1595 100644 --- a/app/javascript/flavours/glitch/components/check.jsx +++ b/app/javascript/flavours/glitch/components/check.jsx @@ -1,3 +1,5 @@ +import React from 'react'; + const Check = () => ( diff --git a/app/javascript/flavours/glitch/components/column.jsx b/app/javascript/flavours/glitch/components/column.jsx index 312a6848b..47293ef18 100644 --- a/app/javascript/flavours/glitch/components/column.jsx +++ b/app/javascript/flavours/glitch/components/column.jsx @@ -1,13 +1,9 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - import { supportsPassiveEvents } from 'detect-passive-events'; - import { scrollTop } from '../scroll'; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -export default class Column extends PureComponent { +export default class Column extends React.PureComponent { static propTypes = { children: PropTypes.node, @@ -41,17 +37,17 @@ export default class Column extends PureComponent { componentDidMount () { if (this.props.bindToDocument) { - document.addEventListener('wheel', this.handleWheel, listenerOptions); + document.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } else { - this.node.addEventListener('wheel', this.handleWheel, listenerOptions); + this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); } } componentWillUnmount () { if (this.props.bindToDocument) { - document.removeEventListener('wheel', this.handleWheel, listenerOptions); + document.removeEventListener('wheel', this.handleWheel); } else { - this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); + this.node.removeEventListener('wheel', this.handleWheel); } } diff --git a/app/javascript/flavours/glitch/components/column_back_button.jsx b/app/javascript/flavours/glitch/components/column_back_button.jsx index 0934d4b33..e9e2615cb 100644 --- a/app/javascript/flavours/glitch/components/column_back_button.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button.jsx @@ -1,13 +1,10 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import Icon from 'flavours/glitch/components/icon'; import { createPortal } from 'react-dom'; -import { FormattedMessage } from 'react-intl'; - -import { Icon } from 'flavours/glitch/components/icon'; - - -export default class ColumnBackButton extends PureComponent { +export default class ColumnBackButton extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -17,15 +14,17 @@ export default class ColumnBackButton extends PureComponent { multiColumn: PropTypes.bool, }; - 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(); + 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 { - router.history.push('/'); + this.context.router.history.push('/'); } }; diff --git a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx index 7b3bac45f..b43d85b3b 100644 --- a/app/javascript/flavours/glitch/components/column_back_button_slim.jsx +++ b/app/javascript/flavours/glitch/components/column_back_button_slim.jsx @@ -1,32 +1,32 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - +import React from 'react'; import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; +import Icon from 'flavours/glitch/components/icon'; -import { Icon } from 'flavours/glitch/components/icon'; - -export default class ColumnBackButtonSlim extends PureComponent { +export default class ColumnBackButtonSlim extends React.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(); + 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 { - router.history.push('/'); + this.context.router.history.push('/'); } }; render () { return (
-
+
diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx index e8c056c0b..3790960dd 100644 --- a/app/javascript/flavours/glitch/components/column_header.jsx +++ b/app/javascript/flavours/glitch/components/column_header.jsx @@ -1,12 +1,9 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; import { createPortal } from 'react-dom'; - -import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; - import classNames from 'classnames'; - -import { Icon } from 'flavours/glitch/components/icon'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, @@ -15,7 +12,8 @@ const messages = defineMessages({ moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, }); -class ColumnHeader extends PureComponent { +export default @injectIntl +class ColumnHeader extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -45,6 +43,20 @@ class ColumnHeader extends 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 }); @@ -62,16 +74,8 @@ class ColumnHeader extends PureComponent { this.props.onMove(1); }; - 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('/'); - } + handleBackClick = (event) => { + this.historyBack(event.shiftKey); }; handleTransitionEnd = () => { @@ -80,9 +84,8 @@ class ColumnHeader extends PureComponent { handlePin = () => { if (!this.props.pinned) { - this.context.router.history.replace('/'); + this.historyBack(); } - this.props.onPin(); }; @@ -215,5 +218,3 @@ class ColumnHeader extends PureComponent { } } - -export default injectIntl(ColumnHeader); diff --git a/app/javascript/flavours/glitch/components/common_counter.jsx b/app/javascript/flavours/glitch/components/common_counter.jsx index 785907bd2..dd9b62de9 100644 --- a/app/javascript/flavours/glitch/components/common_counter.jsx +++ b/app/javascript/flavours/glitch/components/common_counter.jsx @@ -1,7 +1,10 @@ // @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.jsx b/app/javascript/flavours/glitch/components/dismissable_banner.jsx index 21063c9ed..c4968ac3c 100644 --- a/app/javascript/flavours/glitch/components/dismissable_banner.jsx +++ b/app/javascript/flavours/glitch/components/dismissable_banner.jsx @@ -1,17 +1,15 @@ +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' }, }); -class DismissableBanner extends PureComponent { +export default @injectIntl +class DismissableBanner extends React.PureComponent { static propTypes = { id: PropTypes.string.isRequired, @@ -51,5 +49,3 @@ class DismissableBanner extends PureComponent { } } - -export default injectIntl(DismissableBanner); diff --git a/app/javascript/flavours/glitch/components/display_name.jsx b/app/javascript/flavours/glitch/components/display_name.jsx new file mode 100644 index 000000000..19f63ec60 --- /dev/null +++ b/app/javascript/flavours/glitch/components/display_name.jsx @@ -0,0 +1,102 @@ +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/domain.jsx b/app/javascript/flavours/glitch/components/domain.jsx new file mode 100644 index 000000000..e09fa4591 --- /dev/null +++ b/app/javascript/flavours/glitch/components/domain.jsx @@ -0,0 +1,42 @@ +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/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx index 8fe9cb401..f4b6e059f 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx @@ -1,20 +1,16 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent, cloneElement, Children } from 'react'; - -import classNames from 'classnames'; - import ImmutablePropTypes from 'react-immutable-proptypes'; - -import { supportsPassiveEvents } from 'detect-passive-events'; +import IconButton from './icon_button'; import Overlay from 'react-overlays/Overlay'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import classNames from 'classnames'; +import { CircularProgress } from 'flavours/glitch/components/loading_indicator'; -import { CircularProgress } from './circular_progress'; -import { IconButton } from './icon_button'; - -const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; let id = 0; -class DropdownMenu extends PureComponent { +class DropdownMenu extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -39,13 +35,12 @@ class DropdownMenu extends PureComponent { handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); - e.stopPropagation(); } }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); - document.addEventListener('keydown', this.handleKeyDown, { capture: true }); + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('keydown', this.handleKeyDown, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem && this.props.openedViaKeyboard) { @@ -54,8 +49,8 @@ class DropdownMenu extends PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); - document.removeEventListener('keydown', this.handleKeyDown, { capture: true }); + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('keydown', this.handleKeyDown, false); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -120,11 +115,11 @@ class DropdownMenu extends PureComponent { return
  • ; } - const { text, href = '#', target = '_blank', method, dangerous } = option; + const { text, href = '#', target = '_blank', method } = option; return ( -
  • - +
  • + {text}
  • @@ -159,7 +154,7 @@ class DropdownMenu extends PureComponent { } -export default class Dropdown extends PureComponent { +export default class Dropdown extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -290,7 +285,7 @@ export default class Dropdown extends PureComponent { const open = this.state.id === openDropdownId; - const button = children ? cloneElement(Children.only(children), { + const button = children ? React.cloneElement(React.Children.only(children), { onClick: this.handleClick, onMouseDown: this.handleMouseDown, onKeyDown: this.handleButtonKeyDown, @@ -310,7 +305,7 @@ export default class Dropdown extends PureComponent { ); return ( - <> + {button} @@ -333,7 +328,7 @@ export default class Dropdown extends 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 7c9c16713..a1519757d 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,5 +1,4 @@ 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.jsx b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx index 3dbac58b5..fbd473062 100644 --- a/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx +++ b/app/javascript/flavours/glitch/components/edited_timestamp/index.jsx @@ -1,29 +1,23 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage, injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { openModal } from 'flavours/glitch/actions/modal'; -import { Icon } from 'flavours/glitch/components/icon'; -import InlineAccount from 'flavours/glitch/components/inline_account'; -import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; - +import { FormattedMessage } 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 InlineAccount from 'flavours/glitch/components/inline_account'; const mapDispatchToProps = (dispatch, { statusId }) => ({ onItemClick (index) { - dispatch(openModal({ - modalType: 'COMPARE_HISTORY', - modalProps: { index, statusId }, - })); + dispatch(openModal('COMPARE_HISTORY', { index, statusId })); }, }); -class EditedTimestamp extends PureComponent { +export default @connect(null, mapDispatchToProps) +class EditedTimestamp extends React.PureComponent { static propTypes = { statusId: PropTypes.string.isRequired, @@ -39,7 +33,7 @@ class EditedTimestamp extends PureComponent { renderHeader = items => { return ( - + ); }; @@ -61,17 +55,15 @@ class EditedTimestamp extends PureComponent { }; render () { - const { timestamp, intl, statusId } = this.props; + const { timestamp, statusId } = this.props; return ( ); } } - -export default connect(null, mapDispatchToProps)(injectIntl(EditedTimestamp)); diff --git a/app/javascript/flavours/glitch/components/error_boundary.jsx b/app/javascript/flavours/glitch/components/error_boundary.jsx index 2c07c5cf6..8518dfc86 100644 --- a/app/javascript/flavours/glitch/components/error_boundary.jsx +++ b/app/javascript/flavours/glitch/components/error_boundary.jsx @@ -1,16 +1,12 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - import { FormattedMessage } from 'react-intl'; - -import { Helmet } from 'react-helmet'; - -import StackTrace from 'stacktrace-js'; - 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 PureComponent { +export default class ErrorBoundary extends React.PureComponent { static propTypes = { children: PropTypes.node, @@ -76,7 +72,7 @@ export default class ErrorBoundary extends PureComponent { } return ( -
    +

    @@ -113,7 +109,7 @@ export default class ErrorBoundary extends PureComponent { }} + values={{ reload: }} /> { preferencesLink !== undefined && ( diff --git a/app/javascript/flavours/glitch/components/gifv.jsx b/app/javascript/flavours/glitch/components/gifv.jsx new file mode 100644 index 000000000..cf54e738a --- /dev/null +++ b/app/javascript/flavours/glitch/components/gifv.jsx @@ -0,0 +1,77 @@ +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, + lang: 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, lang } = this.props; + const { loading } = this.state; + + return ( +

    + {loading && ( + + )} + +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/hashtag.jsx b/app/javascript/flavours/glitch/components/hashtag.jsx index 422ead01d..422b9a8fa 100644 --- a/app/javascript/flavours/glitch/components/hashtag.jsx +++ b/app/javascript/flavours/glitch/components/hashtag.jsx @@ -1,21 +1,15 @@ // @ts-check -import PropTypes from 'prop-types'; -import { Component } from 'react'; - +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 classNames from 'classnames'; -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 { +class SilentErrorBoundary extends React.Component { static propTypes = { children: PropTypes.node, @@ -41,12 +35,13 @@ class SilentErrorBoundary extends 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}
    , @@ -55,14 +50,12 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => ( /> ); -// @ts-expect-error export const ImmutableHashtag = ({ hashtag }) => ( day.get('uses')).toArray()} /> ); @@ -71,12 +64,11 @@ 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.jsx b/app/javascript/flavours/glitch/components/icon.jsx new file mode 100644 index 000000000..d8a17722f --- /dev/null +++ b/app/javascript/flavours/glitch/components/icon.jsx @@ -0,0 +1,21 @@ +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_button.jsx b/app/javascript/flavours/glitch/components/icon_button.jsx new file mode 100644 index 000000000..10d7926be --- /dev/null +++ b/app/javascript/flavours/glitch/components/icon_button.jsx @@ -0,0 +1,177 @@ +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 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, + }; + + static defaultProps = { + size: 18, + active: false, + disabled: false, + animate: false, + overlay: false, + tabIndex: '0', + ariaHidden: false, + }; + + state = { + activate: false, + deactivate: false, + }; + + componentWillReceiveProps (nextProps) { + if (!nextProps.animate) return; + + if (this.props.active && !nextProps.active) { + this.setState({ activate: false, deactivate: true }); + } else if (!this.props.active && nextProps.active) { + this.setState({ activate: true, deactivate: false }); + } + } + + handleClick = (e) => { + e.preventDefault(); + + if (!this.props.disabled) { + this.props.onClick(e); + } + }; + + handleKeyPress = (e) => { + if (this.props.onKeyPress && !this.props.disabled) { + this.props.onKeyPress(e); + } + }; + + handleMouseDown = (e) => { + if (!this.props.disabled && this.props.onMouseDown) { + this.props.onMouseDown(e); + } + }; + + handleKeyDown = (e) => { + if (!this.props.disabled && this.props.onKeyDown) { + this.props.onKeyDown(e); + } + }; + + 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 = { + fontSize: `${this.props.size}px`, + height: containerSize, + lineHeight: `${this.props.size}px`, + ...this.props.style, + ...(this.props.active ? this.props.activeStyle : {}), + }; + if (!this.props.label) { + style.width = containerSize; + } else { + style.textAlign = 'left'; + } + + const { + active, + className, + disabled, + expanded, + icon, + inverted, + overlay, + tabIndex, + title, + counter, + obfuscateCount, + href, + ariaHidden, + } = this.props; + + const { + activate, + deactivate, + } = this.state; + + const classes = classNames(className, 'icon-button', { + active, + disabled, + inverted, + activate, + deactivate, + overlayed: overlay, + 'icon-button--with-counter': typeof counter !== 'undefined', + }); + + if (typeof counter !== 'undefined') { + style.width = 'auto'; + } + + let contents = ( + + + ); + + if (href && !this.prop) { + contents = ( + + {contents} + + ); + } + + return ( + + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/icon_with_badge.jsx b/app/javascript/flavours/glitch/components/icon_with_badge.jsx new file mode 100644 index 000000000..a42ba4589 --- /dev/null +++ b/app/javascript/flavours/glitch/components/icon_with_badge.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from 'flavours/glitch/components/icon'; + +const formatNumber = num => num > 40 ? '40+' : num; + +const IconWithBadge = ({ id, count, issueBadge, className }) => ( + + + {count > 0 && {formatNumber(count)}} + {issueBadge && } + +); + +IconWithBadge.propTypes = { + id: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + issueBadge: PropTypes.bool, + className: PropTypes.string, +}; + +export default IconWithBadge; diff --git a/app/javascript/flavours/glitch/components/image.jsx b/app/javascript/flavours/glitch/components/image.jsx new file mode 100644 index 000000000..6e81ddf08 --- /dev/null +++ b/app/javascript/flavours/glitch/components/image.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Blurhash from './blurhash'; +import classNames from 'classnames'; + +export default class Image extends React.PureComponent { + + static propTypes = { + src: PropTypes.string, + srcSet: PropTypes.string, + blurhash: PropTypes.string, + className: PropTypes.string, + }; + + state = { + loaded: false, + }; + + handleLoad = () => this.setState({ loaded: true }); + + render () { + const { src, srcSet, blurhash, className } = this.props; + const { loaded } = this.state; + + return ( +
    + {blurhash && } + +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/inline_account.jsx b/app/javascript/flavours/glitch/components/inline_account.jsx index 98e44c05e..2ef1f52cc 100644 --- a/app/javascript/flavours/glitch/components/inline_account.jsx +++ b/app/javascript/flavours/glitch/components/inline_account.jsx @@ -1,10 +1,8 @@ -import { PureComponent } from 'react'; - +import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; - -import { Avatar } from 'flavours/glitch/components/avatar'; import { makeGetAccount } from 'flavours/glitch/selectors'; +import Avatar from 'flavours/glitch/components/avatar'; const makeMapStateToProps = () => { const getAccount = makeGetAccount(); @@ -16,7 +14,8 @@ const makeMapStateToProps = () => { return mapStateToProps; }; -class InlineAccount extends PureComponent { +export default @connect(makeMapStateToProps) +class InlineAccount extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, @@ -33,5 +32,3 @@ class InlineAccount extends PureComponent { } } - -export default connect(makeMapStateToProps)(InlineAccount); diff --git a/app/javascript/flavours/glitch/components/intersection_observer_article.jsx b/app/javascript/flavours/glitch/components/intersection_observer_article.jsx index bef40c07f..77cd66358 100644 --- a/app/javascript/flavours/glitch/components/intersection_observer_article.jsx +++ b/app/javascript/flavours/glitch/components/intersection_observer_article.jsx @@ -1,12 +1,12 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { cloneElement, Component } from 'react'; - -import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; +import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; + // Diff these props in the "unrendered" state const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight']; -export default class IntersectionObserverArticle extends Component { +export default class IntersectionObserverArticle extends React.Component { static propTypes = { intersectionObserverWrapper: PropTypes.object.isRequired, @@ -120,10 +120,10 @@ export default class IntersectionObserverArticle extends Component { aria-posinset={index + 1} aria-setsize={listLength} data-id={id} - tabIndex={0} + tabIndex='0' style={style} > - {children && cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })} + {children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })} ); } diff --git a/app/javascript/flavours/glitch/components/link.jsx b/app/javascript/flavours/glitch/components/link.jsx index 9babe7320..bbec121a8 100644 --- a/app/javascript/flavours/glitch/components/link.jsx +++ b/app/javascript/flavours/glitch/components/link.jsx @@ -2,13 +2,13 @@ // ~ 😘 kibi! // Package imports. -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; // Utils. import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; + // Handlers. const handlers = { @@ -25,7 +25,7 @@ const handlers = { }; // The component. -export default class Link extends PureComponent { +export default class Link extends React.PureComponent { // Constructor. constructor (props) { diff --git a/app/javascript/flavours/glitch/components/load_gap.jsx b/app/javascript/flavours/glitch/components/load_gap.jsx new file mode 100644 index 000000000..6ed9a38c6 --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_gap.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; + +const messages = defineMessages({ + load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, +}); + +export default @injectIntl +class LoadGap extends React.PureComponent { + + static propTypes = { + disabled: PropTypes.bool, + maxId: PropTypes.string, + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleClick = () => { + this.props.onClick(this.props.maxId); + }; + + render () { + const { disabled, intl } = this.props; + + return ( + + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/load_more.jsx b/app/javascript/flavours/glitch/components/load_more.jsx new file mode 100644 index 000000000..ab9428e35 --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_more.jsx @@ -0,0 +1,27 @@ +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_pending.jsx b/app/javascript/flavours/glitch/components/load_pending.jsx new file mode 100644 index 000000000..a75259146 --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_pending.jsx @@ -0,0 +1,22 @@ +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/loading_indicator.jsx b/app/javascript/flavours/glitch/components/loading_indicator.jsx new file mode 100644 index 000000000..59f721c50 --- /dev/null +++ b/app/javascript/flavours/glitch/components/loading_indicator.jsx @@ -0,0 +1,32 @@ +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/logo.jsx b/app/javascript/flavours/glitch/components/logo.jsx index 16ca9f80f..ee5c22496 100644 --- a/app/javascript/flavours/glitch/components/logo.jsx +++ b/app/javascript/flavours/glitch/components/logo.jsx @@ -1,14 +1,10 @@ -import logo from 'mastodon/../images/logo.svg'; +import React from 'react'; -export const WordmarkLogo = () => ( - +const Logo = () => ( + Mastodon ); -export const SymbolLogo = () => ( - Mastodon -); - -export default WordmarkLogo; +export default Logo; diff --git a/app/javascript/flavours/glitch/components/media_attachments.jsx b/app/javascript/flavours/glitch/components/media_attachments.jsx index 4e777437a..b11d3526f 100644 --- a/app/javascript/flavours/glitch/components/media_attachments.jsx +++ b/app/javascript/flavours/glitch/components/media_attachments.jsx @@ -1,12 +1,10 @@ +import React from 'react'; import PropTypes from 'prop-types'; - import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; - -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'; +import Bundle from 'flavours/glitch/features/ui/components/bundle'; +import noop from 'lodash/noop'; export default class MediaAttachments extends ImmutablePureComponent { @@ -52,9 +50,8 @@ export default class MediaAttachments extends ImmutablePureComponent { }; render () { - const { status, width, height, revealed } = this.props; + const { status, lang, 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; @@ -62,15 +59,14 @@ 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 => ( @@ -94,8 +89,8 @@ export default class MediaAttachments extends ImmutablePureComponent { frameRate={video.getIn(['meta', 'original', 'frame_rate'])} blurhash={video.get('blurhash')} src={video.get('url')} - alt={description} - lang={language} + alt={video.get('description')} + lang={lang || status.get('language')} width={width} height={height} inline @@ -112,7 +107,7 @@ export default class MediaAttachments extends ImmutablePureComponent { {Component => ( 0) { - badges.push(ALT); + 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'; + } } - const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); + let thumbnail = ''; if (attachment.get('type') === 'unknown') { return ( -
    - +
    + GIF); - thumbnail = (
    ); } return ( -
    +
    - {visible && thumbnail} - - {badges && ( -
    - {badges} -
    - )}
    ); } } -class MediaGallery extends PureComponent { +export default @injectIntl +class MediaGallery extends React.PureComponent { static propTypes = { sensitive: PropTypes.bool, @@ -262,7 +282,7 @@ class MediaGallery extends PureComponent { window.removeEventListener('resize', this.handleResize); } - UNSAFE_componentWillReceiveProps (nextProps) { + 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) { @@ -270,7 +290,7 @@ class MediaGallery extends PureComponent { } } - componentDidUpdate () { + componentDidUpdate (prevProps) { if (this.node) { this.handleResize(); } @@ -294,7 +314,7 @@ class MediaGallery extends PureComponent { }; handleClick = (index) => { - this.props.onOpenMedia(this.props.media, index, this.props.lang); + this.props.onOpenMedia(this.props.media, index); }; handleRef = (node) => { @@ -308,7 +328,7 @@ class MediaGallery extends PureComponent { _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); @@ -339,10 +359,12 @@ class MediaGallery extends PureComponent { const computedClass = classNames('media-gallery', { 'full-width': fullwidth }); - if (this.isStandaloneEligible()) { // TODO: cropImages setting - style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`; + if (this.isStandaloneEligible() && width) { + style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); + } else if (width) { + style.height = width / (16/9); } else { - style.aspectRatio = '16 / 9'; + return (
    ); } if (this.isStandaloneEligible()) { @@ -384,5 +406,3 @@ class MediaGallery extends PureComponent { } } - -export default injectIntl(MediaGallery); diff --git a/app/javascript/flavours/glitch/components/missing_indicator.jsx b/app/javascript/flavours/glitch/components/missing_indicator.jsx new file mode 100644 index 000000000..08e39c236 --- /dev/null +++ b/app/javascript/flavours/glitch/components/missing_indicator.jsx @@ -0,0 +1,29 @@ +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.jsx b/app/javascript/flavours/glitch/components/modal_root.jsx index a99c51f92..5a5563e87 100644 --- a/app/javascript/flavours/glitch/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/components/modal_root.jsx @@ -1,11 +1,10 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - import 'wicg-inert'; -import { multiply } from 'color-blend'; import { createBrowserHistory } from 'history'; +import { multiply } from 'color-blend'; -export default class ModalRoot extends PureComponent { +export default class ModalRoot extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -63,7 +62,7 @@ export default class ModalRoot extends PureComponent { } } - UNSAFE_componentWillReceiveProps (nextProps) { + componentWillReceiveProps (nextProps) { if (!!nextProps.children && !this.props.children) { this.activeElement = document.activeElement; diff --git a/app/javascript/flavours/glitch/components/name_list.js b/app/javascript/flavours/glitch/components/name_list.js new file mode 100644 index 000000000..c54e29b2a --- /dev/null +++ b/app/javascript/flavours/glitch/components/name_list.js @@ -0,0 +1,117 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl } from 'react-intl'; + +/** + * Displays a single account (or label) as a link. + * + * Noteworthy that onClick gets called as `onClick(account, ev)` if an account is provided, + * but only gets called with `onClick(ev)` if no account was provided. + */ +class NameLink extends React.PureComponent { + + static propTypes = { + account: ImmutablePropTypes.map, + href: PropTypes.string, + onClick: PropTypes.func, + children: PropTypes.string, + }; + + handleClick = (ev) => { + const { account, onClick } = this.props; + onClick(account, ev); + }; + + render() { + const { href, children } = this.props; + return ( +
    + ); + } + +} + +/** + * Displays a list of accounts as a comma-separated, link-ified list of displaynames. + */ +@injectIntl +export default class NameList extends React.PureComponent { + + static propTypes = { + intl: PropTypes.object, + accounts: ImmutablePropTypes.listOf(ImmutablePropTypes.map), + viewMoreHref: PropTypes.string, + onClick: PropTypes.func, + }; + + render() { + const { accounts, intl, viewMoreHref, onClick } = this.props; + + // render a single name if there is only one account + if (accounts.size === 1) { + return ( + + + {accounts.get(0).get('display_name_html') || accounts.get(0).get('username')} + + + ); + } + + // turn a list of accounts into a list (max length 3) of labels + let accountsToIntl; + const hasOthers = accounts.size > 3; + if (hasOthers) { + accountsToIntl = accounts.slice(0, 2).map(acct => acct.get('display_name_html') || acct.get('username')); + accountsToIntl = accountsToIntl.push(intl.formatMessage({ id: 'notifications.others', defaultMessage: 'others' })); + } else { + accountsToIntl = accounts.map(acct => acct.get('display_name_html') || acct.get('username')); + } + + // turn the list of labels into an array of parts, with the correct localization + // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat + const parts = new Intl.ListFormat(intl.locale, { type: 'conjunction' }).formatToParts(accountsToIntl); + + // linkify the list of labels + let elementNum = 0; + return parts.map(({ type, value }) => { + const currentElement = elementNum; + const account = accounts.get(currentElement); + + // if this is a label, linkify it + if (type === 'element') { + elementNum++; + + // special case for the "and others" label + if (hasOthers && currentElement === 2) { + return ( + + {value} + + ); + } + + // return the linkified label + return {value}; + } else { + // if this is a separator, just print it out regularly + return {value}; + } + }); + } + +} diff --git a/app/javascript/flavours/glitch/components/navigation_portal.jsx b/app/javascript/flavours/glitch/components/navigation_portal.jsx index e142a3ec6..90afa1da0 100644 --- a/app/javascript/flavours/glitch/components/navigation_portal.jsx +++ b/app/javascript/flavours/glitch/components/navigation_portal.jsx @@ -1,21 +1,22 @@ -import { PureComponent } from 'react'; - +import React from 'react'; import { Switch, Route, withRouter } from 'react-router-dom'; - -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'; +import Trends from 'flavours/glitch/features/getting_started/containers/trends_container'; +import AccountNavigation from 'flavours/glitch/features/account/navigation'; const DefaultNavigation = () => ( - showTrends ? ( - <> -
    - - - ) : null + <> + {showTrends && ( + <> +
    + + + )} + ); -class NavigationPortal extends PureComponent { +export default @withRouter +class NavigationPortal extends React.PureComponent { render () { return ( @@ -32,5 +33,3 @@ class NavigationPortal extends PureComponent { } } - -export default withRouter(NavigationPortal); diff --git a/app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx b/app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx new file mode 100644 index 000000000..b440c6be2 --- /dev/null +++ b/app/javascript/flavours/glitch/components/not_signed_in_indicator.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +const NotSignedInIndicator = () => ( +
    +
    + +
    +
    +); + +export default NotSignedInIndicator; diff --git a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx index dfc7ac5a8..3c7d67109 100644 --- a/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx +++ b/app/javascript/flavours/glitch/components/notification_purge_buttons.jsx @@ -6,15 +6,11 @@ // 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' }, @@ -23,6 +19,7 @@ const messages = defineMessages({ btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' }, }); +export default @injectIntl class NotificationPurgeButtons extends ImmutablePureComponent { static propTypes = { @@ -40,19 +37,19 @@ class NotificationPurgeButtons extends ImmutablePureComponent { //className='active' return (
    - - - -
    @@ -60,5 +57,3 @@ class NotificationPurgeButtons extends ImmutablePureComponent { } } - -export default injectIntl(NotificationPurgeButtons); diff --git a/app/javascript/flavours/glitch/components/permalink.jsx b/app/javascript/flavours/glitch/components/permalink.jsx index fa33ce066..b09b17eeb 100644 --- a/app/javascript/flavours/glitch/components/permalink.jsx +++ 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 PureComponent { +export default class Permalink extends React.PureComponent { static contextTypes = { router: PropTypes.object, @@ -24,7 +24,9 @@ export default class Permalink extends PureComponent { if (this.context.router) { e.preventDefault(); - this.context.router.history.push(this.props.to); + let state = { ...this.context.router.history.location.state }; + state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; + this.context.router.history.push(this.props.to, state); } } }; diff --git a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx index 1a290c91d..8bfdf343c 100644 --- a/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/flavours/glitch/components/picture_in_picture_placeholder.jsx @@ -1,27 +1,65 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - +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'; -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 { +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 ( -
    +
    @@ -29,5 +67,3 @@ class PictureInPicturePlaceholder extends PureComponent { } } - -export default connect()(PictureInPicturePlaceholder); diff --git a/app/javascript/flavours/glitch/components/poll.jsx b/app/javascript/flavours/glitch/components/poll.jsx index eca7b5c52..8b799309b 100644 --- a/app/javascript/flavours/glitch/components/poll.jsx +++ b/app/javascript/flavours/glitch/components/poll.jsx @@ -1,21 +1,15 @@ +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 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 { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; import Motion from 'flavours/glitch/features/ui/util/optional_motion'; - -import { RelativeTimestamp } from './relative_timestamp'; - +import spring from 'react-motion/lib/spring'; +import escapeTextContentForBrowser from 'escape-html'; +import emojify from 'flavours/glitch/features/emoji/emoji'; +import RelativeTimestamp from './relative_timestamp'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ closed: { @@ -37,6 +31,7 @@ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { return obj; }, {}); +export default @injectIntl class Poll extends ImmutablePureComponent { static contextTypes = { @@ -58,9 +53,9 @@ class Poll extends ImmutablePureComponent { }; static getDerivedStateFromProps (props, state) { - const { poll } = props; + const { poll, intl } = props; const expires_at = poll.get('expires_at'); - const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now(); + const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now(); return (expired === state.expired) ? null : { expired }; } @@ -77,10 +72,10 @@ class Poll extends ImmutablePureComponent { } _setupTimer () { - const { poll } = this.props; + const { poll, intl } = this.props; clearTimeout(this._timer); if (!this.state.expired) { - const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now(); + const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now(); this._timer = setTimeout(() => { this.setState({ expired: true }); }, delay); @@ -139,12 +134,10 @@ class Poll extends ImmutablePureComponent { const active = !!this.state.selected[`${optionIndex}`]; const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); - const title = option.getIn(['translation', 'title']) || option.get('title'); - let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml'); - - if (!titleHtml) { + let titleEmojified = option.get('title_emojified'); + if (!titleEmojified) { const emojiMap = makeEmojiMap(poll); - titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap); + titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap); } return ( @@ -162,11 +155,11 @@ class Poll extends ImmutablePureComponent { {!showResults && ( @@ -185,7 +178,7 @@ class Poll extends ImmutablePureComponent { {!!voted && @@ -241,5 +234,3 @@ class Poll extends ImmutablePureComponent { } } - -export default injectIntl(Poll); diff --git a/app/javascript/flavours/glitch/components/radio_button.jsx b/app/javascript/flavours/glitch/components/radio_button.jsx new file mode 100644 index 000000000..0496fa286 --- /dev/null +++ b/app/javascript/flavours/glitch/components/radio_button.jsx @@ -0,0 +1,35 @@ +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/regeneration_indicator.jsx b/app/javascript/flavours/glitch/components/regeneration_indicator.jsx index 78844f389..68ce09df9 100644 --- a/app/javascript/flavours/glitch/components/regeneration_indicator.jsx +++ 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.jsx b/app/javascript/flavours/glitch/components/relative_timestamp.jsx new file mode 100644 index 000000000..57f8a5bf2 --- /dev/null +++ b/app/javascript/flavours/glitch/components/relative_timestamp.jsx @@ -0,0 +1,199 @@ +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 = { + hourCycle: 'h23', + 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 = date.toLocaleString(undefined, shortDateFormatOptions); + } else { + relativeTime = date.toLocaleString(undefined, { ...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/scrollable_list.jsx b/app/javascript/flavours/glitch/components/scrollable_list.jsx index 8d18c2081..ae1ba3037 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.jsx +++ b/app/javascript/flavours/glitch/components/scrollable_list.jsx @@ -1,34 +1,26 @@ +import React, { PureComponent } from 'react'; +import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import PropTypes from 'prop-types'; -import { Children, cloneElement, PureComponent } from 'react'; - -import classNames from 'classnames'; - +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 classNames from 'classnames'; +import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; +import LoadingIndicator from './loading_indicator'; 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 = { @@ -99,19 +91,15 @@ 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; - this._getScrollingElement().scrollTop = newScrollTop; + if (this.props.bindToDocument) { + document.scrollingElement.scrollTop = newScrollTop; + } else { + this.node.scrollTop = newScrollTop; + } } }; @@ -119,7 +107,6 @@ class ScrollableList extends PureComponent { if (this.mouseIdleTimer === null) { return; } - clearTimeout(this.mouseIdleTimer); this.mouseIdleTimer = null; }; @@ -127,13 +114,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); @@ -148,7 +135,6 @@ class ScrollableList extends PureComponent { if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) { this.setScrollTop(0); } - this.mouseMovedRecently = false; this.scrollToTopOnMouseIdle = false; }; @@ -156,7 +142,6 @@ class ScrollableList extends PureComponent { componentDidMount () { this.attachScrollListener(); this.attachIntersectionObserver(); - attachFullscreenListener(this.onFullScreenChange); // Handle initial scroll position @@ -172,15 +157,15 @@ class ScrollableList extends PureComponent { }; getScrollTop = () => { - return this._getScrollingElement().scrollTop; + return this.props.bindToDocument ? document.scrollingElement.scrollTop : this.node.scrollTop; }; getScrollHeight = () => { - return this._getScrollingElement().scrollHeight; + return this.props.bindToDocument ? document.scrollingElement.scrollHeight : this.node.scrollHeight; }; getClientHeight = () => { - return this._getScrollingElement().clientHeight; + return this.props.bindToDocument ? document.scrollingElement.clientHeight : this.node.clientHeight; }; updateScrollBottom = (snapshot) => { @@ -189,9 +174,13 @@ class ScrollableList extends PureComponent { this.setScrollTop(newScrollTop); }; - getSnapshotBeforeUpdate (prevProps) { - const someItemInserted = Children.count(prevProps.children) > 0 && - Children.count(prevProps.children) < Children.count(this.props.children) && + 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) && this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props); const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0); @@ -210,17 +199,10 @@ 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); } @@ -229,13 +211,10 @@ class ScrollableList extends PureComponent { }; attachIntersectionObserver () { - let nodeOptions = { + this.intersectionObserverWrapper.connect({ root: this.node, rootMargin: '300% 0px', - }; - - this.intersectionObserverWrapper - .connect(this.props.bindToDocument ? {} : nodeOptions); + }); } detachIntersectionObserver () { @@ -245,20 +224,20 @@ class ScrollableList extends PureComponent { attachScrollListener () { if (this.props.bindToDocument) { document.addEventListener('scroll', this.handleScroll); - document.addEventListener('wheel', this.handleWheel, listenerOptions); + document.addEventListener('wheel', this.handleWheel); } else { this.node.addEventListener('scroll', this.handleScroll); - this.node.addEventListener('wheel', this.handleWheel, listenerOptions); + this.node.addEventListener('wheel', this.handleWheel); } } detachScrollListener () { if (this.props.bindToDocument) { document.removeEventListener('scroll', this.handleScroll); - document.removeEventListener('wheel', this.handleWheel, listenerOptions); + document.removeEventListener('wheel', this.handleWheel); } else { this.node.removeEventListener('scroll', this.handleScroll); - this.node.removeEventListener('wheel', this.handleWheel, listenerOptions); + this.node.removeEventListener('wheel', this.handleWheel); } } @@ -299,7 +278,7 @@ class ScrollableList extends PureComponent { render () { const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; - const childrenCount = Children.count(children); + const childrenCount = React.Children.count(children); const loadMore = (hasMore && onLoadMore) ? : null; const loadPending = (numPending > 0) ? : null; @@ -325,7 +304,7 @@ class ScrollableList extends PureComponent { {loadPending} - {Children.map(this.props.children, (child, index) => ( + {React.Children.map(this.props.children, (child, index) => ( - {cloneElement(child, { + {React.cloneElement(child, { getScrollPosition: this.getScrollPosition, updateScrollBottom: this.updateScrollBottom, cachedMediaWidth: this.state.cachedMediaWidth, @@ -373,5 +352,3 @@ class ScrollableList extends PureComponent { } } - -export default connect(mapStateToProps, null, null, { forwardRef: true })(ScrollableList); diff --git a/app/javascript/flavours/glitch/components/server_banner.jsx b/app/javascript/flavours/glitch/components/server_banner.jsx index d16b48d04..36e0ff238 100644 --- a/app/javascript/flavours/glitch/components/server_banner.jsx +++ b/app/javascript/flavours/glitch/components/server_banner.jsx @@ -1,18 +1,14 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - +import React 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)' }, @@ -22,7 +18,9 @@ const mapStateToProps = state => ({ server: state.getIn(['server', 'server']), }); -class ServerBanner extends PureComponent { +export default @connect(mapStateToProps) +@injectIntl +class ServerBanner extends React.PureComponent { static propTypes = { server: PropTypes.object, @@ -45,7 +43,7 @@ class ServerBanner extends PureComponent { {domain}, mastodon:
    Mastodon }} />
    - +
    {isLoading ? ( @@ -63,7 +61,7 @@ class ServerBanner extends PureComponent {

    - +
    @@ -93,5 +91,3 @@ class ServerBanner extends PureComponent { } } - -export default connect(mapStateToProps)(injectIntl(ServerBanner)); diff --git a/app/javascript/flavours/glitch/components/setting_text.jsx b/app/javascript/flavours/glitch/components/setting_text.jsx index 79d4bf8ea..3a21a0601 100644 --- a/app/javascript/flavours/glitch/components/setting_text.jsx +++ b/app/javascript/flavours/glitch/components/setting_text.jsx @@ -1,9 +1,8 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - import ImmutablePropTypes from 'react-immutable-proptypes'; -export default class SettingText extends PureComponent { +export default class SettingText extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, diff --git a/app/javascript/flavours/glitch/components/short_number.jsx b/app/javascript/flavours/glitch/components/short_number.jsx index 0ddd26e78..535c17727 100644 --- a/app/javascript/flavours/glitch/components/short_number.jsx +++ b/app/javascript/flavours/glitch/components/short_number.jsx @@ -1,9 +1,7 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { memo } from 'react'; - -import { FormattedMessage, FormattedNumber } from 'react-intl'; - import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; +import { FormattedMessage, FormattedNumber } from 'react-intl'; // @ts-check /** @@ -26,6 +24,7 @@ import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers'; /** * Component that renders short big number to a shorter version + * * @param {ShortNumberProps} param0 Props for the component * @returns {JSX.Element} Rendered number */ @@ -33,14 +32,17 @@ 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; @@ -59,6 +61,7 @@ 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 */ @@ -111,4 +114,4 @@ ShortNumberCounter.propTypes = { value: PropTypes.arrayOf(PropTypes.number), }; -export default memo(ShortNumber); +export default React.memo(ShortNumber); diff --git a/app/javascript/flavours/glitch/components/skeleton.jsx b/app/javascript/flavours/glitch/components/skeleton.jsx new file mode 100644 index 000000000..6a17ffb26 --- /dev/null +++ b/app/javascript/flavours/glitch/components/skeleton.jsx @@ -0,0 +1,11 @@ +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/spoilers.jsx b/app/javascript/flavours/glitch/components/spoilers.jsx new file mode 100644 index 000000000..75e4ec3a1 --- /dev/null +++ b/app/javascript/flavours/glitch/components/spoilers.jsx @@ -0,0 +1,52 @@ +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.jsx b/app/javascript/flavours/glitch/components/status.jsx index d6e1623a0..010ba1750 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -1,46 +1,37 @@ -import PropTypes from 'prop-types'; - -import { injectIntl, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; - -import { List as ImmutableList } from 'immutable'; +import React from 'react'; 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, visibleReactions } 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 PropTypes from 'prop-types'; +import StatusPrepend from './status_prepend'; import StatusHeader from './status_header'; import StatusIcons from './status_icons'; -import StatusPrepend from './status_prepend'; +import StatusContent from './status_content'; +import StatusActionBar from './status_action_bar'; import StatusReactions from './status_reactions'; +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, visibleReactions } from 'flavours/glitch/initial_state'; +import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; +import { List as ImmutableList } from 'immutable'; -const domParser = new DOMParser(); +// 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'; 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, - spoilerText && !expanded ? spoilerText : contentText, - intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), + status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length), + new Date(status.get('created_at')).toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), status.getIn(['account', 'acct']), ]; @@ -67,6 +58,7 @@ export const defaultMediaVisibility = (status, settings) => { return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all'); }; +export default @injectIntl class Status extends ImmutablePureComponent { static contextTypes = { @@ -79,10 +71,8 @@ class Status extends ImmutablePureComponent { id: PropTypes.string, status: ImmutablePropTypes.map, account: PropTypes.oneOfType([ImmutablePropTypes.map, ImmutablePropTypes.listOf(ImmutablePropTypes.map)]), - previousId: PropTypes.string, - nextInReplyToId: PropTypes.string, - rootId: PropTypes.string, onReply: PropTypes.func, + onQuote: PropTypes.func, onFavourite: PropTypes.func, onReblog: PropTypes.func, onBookmark: PropTypes.func, @@ -148,9 +138,6 @@ class Status extends ImmutablePureComponent { 'expanded', 'unread', 'pictureInPicture', - 'previousId', - 'nextInReplyToId', - 'rootId', ]; updateOnStates = [ @@ -295,7 +282,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() { + getSnapshotBeforeUpdate (prevProps, prevState) { if (!this.props.getScrollPosition) return null; const { muted, hidden, status, settings } = this.props; @@ -310,7 +297,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); } @@ -381,7 +368,9 @@ class Status extends ImmutablePureComponent { status.getIn(['reblog', 'id'], status.get('id')) }`; } - router.history.push(destination); + let state = { ...router.history.location.state }; + state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; + router.history.push(destination, state); } e.preventDefault(); } @@ -401,14 +390,11 @@ class Status extends ImmutablePureComponent { handleOpenVideo = (options) => { const { status } = this.props; - const lang = status.getIn(['translation', 'language']) || status.get('language'); - this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options); + this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); }; handleOpenMedia = (media, index) => { - const { status } = this.props; - const lang = status.getIn(['translation', 'language']) || status.get('language'); - this.props.onOpenMedia(status.get('id'), media, index, lang); + this.props.onOpenMedia(this.props.status.get('id'), media, index); }; handleHotkeyOpenMedia = e => { @@ -418,11 +404,10 @@ 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]), lang, { startTime: 0 }); + onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 }); } else { - onOpenMedia(statusId, status.get('media_attachments'), 0, lang); + onOpenMedia(statusId, status.get('media_attachments'), 0); } } }; @@ -456,12 +441,16 @@ class Status extends ImmutablePureComponent { }; 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')}`); + this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state); }; handleHotkeyOpenProfile = () => { - this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); + 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); }; handleHotkeyMoveUp = e => { @@ -472,7 +461,7 @@ class Status extends ImmutablePureComponent { this.props.onMoveDown(this.props.containerId || this.props.id, e.target.getAttribute('data-featured')); }; - handleHotkeyCollapse = () => { + handleHotkeyCollapse = e => { if (!this.props.settings.getIn(['collapsed', 'enabled'])) return; @@ -534,12 +523,9 @@ class Status extends ImmutablePureComponent { unread, featured, pictureInPicture, - previousId, - nextInReplyToId, - rootId, ...other } = this.props; - const { isCollapsed } = this.state; + const { isCollapsed, forceFilter } = this.state; let background = null; let attachments = null; @@ -582,12 +568,10 @@ 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')}
    @@ -595,11 +579,7 @@ 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, @@ -608,7 +588,7 @@ class Status extends ImmutablePureComponent { return ( -
    +
    : {matchedFilters.join(', ')}. {' '} + ); + } + +} + +class Emoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.string.isRequired, + hovered: PropTypes.bool.isRequired, + url: PropTypes.string, + staticUrl: PropTypes.string, + }; + + render() { + const { emoji, hovered, url, staticUrl } = this.props; + + if (unicodeMapping[emoji]) { + const { filename, shortCode } = unicodeMapping[this.props.emoji]; + const title = shortCode ? `:${shortCode}:` : ''; + + return ( + {emoji} + ); + } else { + const filename = (autoPlayGif || hovered) ? url : staticUrl; + const shortCode = `:${emoji}:`; + + return ( + {shortCode} + ); + } + } + +} diff --git a/app/javascript/flavours/glitch/components/status_visibility_icon.jsx b/app/javascript/flavours/glitch/components/status_visibility_icon.jsx index ad84af4de..07d56c7a8 100644 --- a/app/javascript/flavours/glitch/components/status_visibility_icon.jsx +++ b/app/javascript/flavours/glitch/components/status_visibility_icon.jsx @@ -1,19 +1,18 @@ // Package imports // +import React from 'react'; import PropTypes from 'prop-types'; - import { defineMessages, injectIntl } from 'react-intl'; - import ImmutablePureComponent from 'react-immutable-pure-component'; - -import { Icon } from 'flavours/glitch/components/icon'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ public: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, + private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, }); +export default @injectIntl class VisibilityIcon extends ImmutablePureComponent { static propTypes = { @@ -50,5 +49,3 @@ class VisibilityIcon extends ImmutablePureComponent { } } - -export default injectIntl(VisibilityIcon); diff --git a/app/javascript/flavours/glitch/components/timeline_hint.jsx b/app/javascript/flavours/glitch/components/timeline_hint.jsx new file mode 100644 index 000000000..fb55a62cc --- /dev/null +++ b/app/javascript/flavours/glitch/components/timeline_hint.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +const TimelineHint = ({ resource, url }) => ( + +); + +TimelineHint.propTypes = { + resource: PropTypes.node.isRequired, + url: PropTypes.string.isRequired, +}; + +export default TimelineHint; diff --git a/app/javascript/flavours/glitch/containers/account_container.jsx b/app/javascript/flavours/glitch/containers/account_container.jsx index f20454585..5b57d730f 100644 --- a/app/javascript/flavours/glitch/containers/account_container.jsx +++ b/app/javascript/flavours/glitch/containers/account_container.jsx @@ -1,7 +1,8 @@ -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - +import React from 'react'; import { connect } from 'react-redux'; - +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import Account from 'flavours/glitch/components/account'; import { followAccount, unfollowAccount, @@ -12,9 +13,7 @@ import { } from 'flavours/glitch/actions/accounts'; import { openModal } from 'flavours/glitch/actions/modal'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; -import Account from 'flavours/glitch/components/account'; import { unfollowModal } from 'flavours/glitch/initial_state'; -import { makeGetAccount } from 'flavours/glitch/selectors'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, @@ -35,13 +34,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (unfollowModal) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - }, + dispatch(openModal('CONFIRM', { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), })); } else { dispatch(unfollowAccount(account.get('id'))); diff --git a/app/javascript/flavours/glitch/containers/admin_component.jsx b/app/javascript/flavours/glitch/containers/admin_component.jsx index 06c846f4d..64dabac8b 100644 --- a/app/javascript/flavours/glitch/containers/admin_component.jsx +++ b/app/javascript/flavours/glitch/containers/admin_component.jsx @@ -1,19 +1,23 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from 'mastodon/locales'; -import { IntlProvider } from 'flavours/glitch/locales'; +const { localeData, messages } = getLocale(); +addLocaleData(localeData); -export default class AdminComponent extends PureComponent { +export default class AdminComponent extends React.PureComponent { static propTypes = { + locale: PropTypes.string.isRequired, children: PropTypes.node.isRequired, }; render () { - const { children } = this.props; + const { locale, children } = this.props; return ( - + {children} ); diff --git a/app/javascript/flavours/glitch/containers/compose_container.jsx b/app/javascript/flavours/glitch/containers/compose_container.jsx index f92bf9797..1e49b89a0 100644 --- a/app/javascript/flavours/glitch/containers/compose_container.jsx +++ b/app/javascript/flavours/glitch/containers/compose_container.jsx @@ -1,13 +1,18 @@ -import { PureComponent } from 'react'; - +import React from 'react'; import { Provider } from 'react-redux'; - -import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; +import PropTypes from 'prop-types'; +import configureStore from 'flavours/glitch/store/configureStore'; import { hydrateStore } from 'flavours/glitch/actions/store'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from 'mastodon/locales'; import Compose from 'flavours/glitch/features/standalone/compose'; import initialState from 'flavours/glitch/initial_state'; -import { IntlProvider } from 'flavours/glitch/locales'; -import { store } from 'flavours/glitch/store'; +import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +const store = configureStore(); if (initialState) { store.dispatch(hydrateStore(initialState)); @@ -15,11 +20,17 @@ if (initialState) { store.dispatch(fetchCustomEmojis()); -export default class ComposeContainer extends PureComponent { +export default class TimelineContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + }; render () { + const { locale } = this.props; + return ( - + diff --git a/app/javascript/flavours/glitch/containers/domain_container.jsx b/app/javascript/flavours/glitch/containers/domain_container.jsx index c719a5775..e92e102ab 100644 --- a/app/javascript/flavours/glitch/containers/domain_container.jsx +++ b/app/javascript/flavours/glitch/containers/domain_container.jsx @@ -1,30 +1,27 @@ -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - +import React from 'react'; import { connect } from 'react-redux'; - import { blockDomain, unblockDomain } from '../actions/domain_blocks'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Domain from '../components/domain'; import { openModal } from '../actions/modal'; -import { Domain } from '../components/domain'; const messages = defineMessages({ blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' }, }); const makeMapStateToProps = () => { - const mapStateToProps = () => ({}); + const mapStateToProps = (state, { }) => ({ + }); return mapStateToProps; }; const mapDispatchToProps = (dispatch, { intl }) => ({ onBlockDomain (domain) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: {domain} }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), - }, + dispatch(openModal('CONFIRM', { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), })); }, diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index da67602b5..43ce8ca63 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -1,9 +1,7 @@ -import { connect } from 'react-redux'; - import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; import { openModal, closeModal } from 'flavours/glitch/actions/modal'; +import { connect } from 'react-redux'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; - import { isUserTouching } from '../is_mobile'; const mapStateToProps = state => ({ @@ -13,21 +11,15 @@ const mapStateToProps = state => ({ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ onOpen(id, onItemClick, keyboard) { - dispatch(isUserTouching() ? openModal({ - modalType: 'ACTIONS', - modalProps: { - status, - actions: items, - onClick: onItemClick, - }, + dispatch(isUserTouching() ? openModal('ACTIONS', { + status, + actions: items, + onClick: onItemClick, }) : openDropdownMenu(id, keyboard, scrollKey)); }, onClose(id) { - dispatch(closeModal({ - modalType: 'ACTIONS', - ignoreFocus: false, - })); + dispatch(closeModal('ACTIONS')); dispatch(closeDropdownMenu(id)); }, }); diff --git a/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js index 11aedd527..f2741f2d4 100644 --- a/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js +++ b/app/javascript/flavours/glitch/containers/intersection_observer_article_container.js @@ -1,7 +1,6 @@ import { connect } from 'react-redux'; - -import { setHeight } from 'flavours/glitch/actions/height_cache'; import IntersectionObserverArticle from 'flavours/glitch/components/intersection_observer_article'; +import { setHeight } from 'flavours/glitch/actions/height_cache'; const makeMapStateToProps = (state, props) => ({ cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]), diff --git a/app/javascript/flavours/glitch/containers/mastodon.jsx b/app/javascript/flavours/glitch/containers/mastodon.jsx index ae2eb0b60..dd7623a81 100644 --- a/app/javascript/flavours/glitch/containers/mastodon.jsx +++ b/app/javascript/flavours/glitch/containers/mastodon.jsx @@ -1,25 +1,26 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - +import React from 'react'; import { Helmet } from 'react-helmet'; -import { BrowserRouter, Route } from 'react-router-dom'; - +import { IntlProvider, addLocaleData } from 'react-intl'; import { Provider as ReduxProvider } from 'react-redux'; - +import { BrowserRouter, Route } from 'react-router-dom'; import { ScrollContext } from 'react-router-scroll-4'; - +import configureStore from 'flavours/glitch/store/configureStore'; +import UI from 'flavours/glitch/features/ui'; import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; -import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings'; import { hydrateStore } from 'flavours/glitch/actions/store'; +import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_settings'; import { connectUserStream } from 'flavours/glitch/actions/streaming'; import ErrorBoundary from 'flavours/glitch/components/error_boundary'; -import UI from 'flavours/glitch/features/ui'; import initialState, { title as siteTitle } from 'flavours/glitch/initial_state'; -import { IntlProvider } from 'flavours/glitch/locales'; -import { store } from 'flavours/glitch/store'; +import { getLocale } from 'locales'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; +export const store = configureStore(); const hydrateAction = hydrateStore(initialState); store.dispatch(hydrateAction); @@ -38,7 +39,11 @@ const createIdentityContext = state => ({ permissions: state.role ? state.role.permissions : 0, }); -export default class Mastodon extends PureComponent { +export default class Mastodon extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + }; static childContextTypes = { identity: PropTypes.shape({ @@ -75,8 +80,10 @@ export default class Mastodon extends PureComponent { } render () { + const { locale } = this.props; + return ( - + diff --git a/app/javascript/flavours/glitch/containers/media_container.jsx b/app/javascript/flavours/glitch/containers/media_container.jsx index 52aac5ebe..37b5484e6 100644 --- a/app/javascript/flavours/glitch/containers/media_container.jsx +++ b/app/javascript/flavours/glitch/containers/media_container.jsx @@ -1,45 +1,47 @@ +import React, { PureComponent, Fragment } from 'react'; +import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; -import { createPortal } from 'react-dom'; - +import { IntlProvider, addLocaleData } from 'react-intl'; import { fromJS } from 'immutable'; - -import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; +import { getLocale } from 'mastodon/locales'; +import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; import MediaGallery from 'flavours/glitch/components/media_gallery'; -import ModalRoot from 'flavours/glitch/components/modal_root'; import Poll from 'flavours/glitch/components/poll'; -import Audio from 'flavours/glitch/features/audio'; -import Card from 'flavours/glitch/features/status/components/card'; +import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; +import ModalRoot from 'flavours/glitch/components/modal_root'; import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; import Video from 'flavours/glitch/features/video'; -import { IntlProvider } from 'flavours/glitch/locales'; -import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; +import Card from 'flavours/glitch/features/status/components/card'; +import Audio from 'flavours/glitch/features/audio'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; export default class MediaContainer extends PureComponent { static propTypes = { + locale: PropTypes.string.isRequired, components: PropTypes.object.isRequired, }; state = { media: null, index: null, - lang: null, time: null, backgroundColor: null, options: null, }; - handleOpenMedia = (media, index, lang) => { + handleOpenMedia = (media, index) => { document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media, index, lang }); + this.setState({ media, index }); }; - handleOpenVideo = (lang, options) => { + handleOpenVideo = (options) => { const { components } = this.props; const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props')); const mediaList = fromJS(media); @@ -47,12 +49,12 @@ export default class MediaContainer extends PureComponent { document.body.classList.add('with-modals--active'); document.documentElement.style.marginRight = `${getScrollbarWidth()}px`; - this.setState({ media: mediaList, lang, options }); + this.setState({ media: mediaList, options }); }; handleCloseMedia = () => { document.body.classList.remove('with-modals--active'); - document.documentElement.style.marginRight = '0'; + document.documentElement.style.marginRight = 0; this.setState({ media: null, @@ -68,18 +70,11 @@ export default class MediaContainer extends PureComponent { }; render () { - const { components } = this.props; - - let handleOpenVideo; - - // Don't offer to expand the video in a lightbox if we're in a frame - if (window.self === window.top) { - handleOpenVideo = this.handleOpenVideo; - } + const { locale, components } = this.props; return ( - - <> + + {[].map.call(components, (component, i) => { const componentName = component.getAttribute('data-component'); const Component = MEDIA_COMPONENTS[componentName]; @@ -93,13 +88,13 @@ export default class MediaContainer extends PureComponent { ...(componentName === 'Video' ? { componentIndex: i, - onOpenVideo: handleOpenVideo, + onOpenVideo: this.handleOpenVideo, } : { onOpenMedia: this.handleOpenMedia, }), }); - return createPortal( + return ReactDOM.createPortal( , component, ); @@ -110,7 +105,6 @@ export default class MediaContainer extends PureComponent { )} - + ); } diff --git a/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js index 144d77f13..2570cf4a5 100644 --- a/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js +++ b/app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js @@ -1,16 +1,15 @@ // Package imports. +import { connect } from 'react-redux'; import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - // Our imports. -import { openModal } from 'flavours/glitch/actions/modal'; +import NotificationPurgeButtons from 'flavours/glitch/components/notification_purge_buttons'; import { deleteMarkedNotifications, enterNotificationClearingMode, markAllNotifications, } from 'flavours/glitch/actions/notifications'; -import NotificationPurgeButtons from 'flavours/glitch/components/notification_purge_buttons'; +import { openModal } from 'flavours/glitch/actions/modal'; const messages = defineMessages({ clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' }, @@ -23,13 +22,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onDeleteMarked() { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(deleteMarkedNotifications()), - }, + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.clearMessage), + confirm: intl.formatMessage(messages.clearConfirm), + onConfirm: () => dispatch(deleteMarkedNotifications()), })); }, diff --git a/app/javascript/flavours/glitch/containers/poll_container.js b/app/javascript/flavours/glitch/containers/poll_container.js index e25dd0614..345351cc6 100644 --- a/app/javascript/flavours/glitch/containers/poll_container.js +++ b/app/javascript/flavours/glitch/containers/poll_container.js @@ -1,9 +1,8 @@ import { connect } from 'react-redux'; - import { debounce } from 'lodash'; -import { fetchPoll, vote } from 'flavours/glitch/actions/polls'; import Poll from 'flavours/glitch/components/poll'; +import { fetchPoll, vote } from 'flavours/glitch/actions/polls'; const mapDispatchToProps = (dispatch, { pollId }) => ({ refresh: debounce( diff --git a/app/javascript/flavours/glitch/containers/status_container.jsx b/app/javascript/flavours/glitch/containers/status_container.jsx index ddb9ec87a..e5e6d5c8b 100644 --- a/app/javascript/flavours/glitch/containers/status_container.jsx +++ b/app/javascript/flavours/glitch/containers/status_container.jsx @@ -1,17 +1,13 @@ -import { defineMessages, injectIntl } from 'react-intl'; - import { connect } from 'react-redux'; - -import { initBlockModal } from 'flavours/glitch/actions/blocks'; -import { initBoostModal } from 'flavours/glitch/actions/boosts'; +import Status from 'flavours/glitch/components/status'; +import { List as ImmutableList } from 'immutable'; +import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; import { replyCompose, + quoteCompose, mentionCompose, directCompose, } from 'flavours/glitch/actions/compose'; -import { - initAddFilter, -} from 'flavours/glitch/actions/filters'; import { reblog, favourite, @@ -24,11 +20,6 @@ import { addReaction, removeReaction, } from 'flavours/glitch/actions/interactions'; -import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; -import { openModal } from 'flavours/glitch/actions/modal'; -import { initMuteModal } from 'flavours/glitch/actions/mutes'; -import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; -import { initReport } from 'flavours/glitch/actions/reports'; import { muteStatus, unmuteStatus, @@ -39,21 +30,35 @@ import { translateStatus, undoStatusTranslation, } from 'flavours/glitch/actions/statuses'; -import Status from 'flavours/glitch/components/status'; +import { + initAddFilter, +} from 'flavours/glitch/actions/filters'; +import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import { initBlockModal } from 'flavours/glitch/actions/blocks'; +import { initReport } from 'flavours/glitch/actions/reports'; +import { initBoostModal } from 'flavours/glitch/actions/boosts'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture'; +import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state'; -import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; - +import { filterEditLink } from 'flavours/glitch/utils/backend_links'; import { showAlertForError } from '../actions/alerts'; +import AccountContainer from 'flavours/glitch/containers/account_container'; +import Spoilers from '../components/spoilers'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, - redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, + redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' }, editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, + quoteMessage: { id: 'confirmations.quote.message', defaultMessage: 'Quoting now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' }, matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' }, @@ -83,8 +88,7 @@ const makeMapStateToProps = () => { return { containerId: props.containerId || props.id, // Should match reblogStatus's id for reblogs status: status, - nextInReplyToId: props.nextId ? state.getIn(['statuses', props.nextId, 'in_reply_to_id']) : null, - account: account || props.account, + account: props.account || account, settings: state.get('local_settings'), prepend: prepend || props.prepend, pictureInPicture: getPictureInPicture(state, props), @@ -101,14 +105,11 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ let state = getState(); if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.replyMessage), - confirm: intl.formatMessage(messages.replyConfirm), - onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), - onConfirm: () => dispatch(replyCompose(status, router)), - }, + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.replyMessage), + confirm: intl.formatMessage(messages.replyConfirm), + onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), + onConfirm: () => dispatch(replyCompose(status, router)), })); } else { dispatch(replyCompose(status, router)); @@ -116,6 +117,23 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }); }, + onQuote (status, router) { + dispatch((_, getState) => { + let state = getState(); + + if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.quoteMessage), + confirm: intl.formatMessage(messages.quoteConfirm), + onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)), + onConfirm: () => dispatch(quoteCompose(status, router)), + })); + } else { + dispatch(quoteCompose(status, router)); + } + }); + }, + onModalReblog (status, privacy) { if (status.get('reblogged')) { dispatch(unreblog(status)); @@ -156,13 +174,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ if (e.shiftKey || !favouriteModal) { this.onModalFavourite(status); } else { - dispatch(openModal({ - modalType: 'FAVOURITE', - modalProps: { - status, - onFavourite: this.onModalFavourite, - }, - })); + dispatch(openModal('FAVOURITE', { status, onFavourite: this.onModalFavourite })); } } }, @@ -184,12 +196,9 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onEmbed (status) { - dispatch(openModal({ - modalType: 'EMBED', - modalProps: { - url: status.get('url'), - onError: error => dispatch(showAlertForError(error)), - }, + dispatch(openModal('EMBED', { + url: status.get('url'), + onError: error => dispatch(showAlertForError(error)), })); }, @@ -197,13 +206,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ if (!deleteModal) { dispatch(deleteStatus(status.get('id'), history, withRedraft)); } else { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), - confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), - onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), - }, + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage), + confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm), + onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)), })); } }, @@ -212,13 +218,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ dispatch((_, getState) => { let state = getState(); if (state.getIn(['compose', 'text']).trim().length !== 0) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.editMessage), - confirm: intl.formatMessage(messages.editConfirm), - onConfirm: () => dispatch(editStatus(status.get('id'), history)), - }, + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.editMessage), + confirm: intl.formatMessage(messages.editConfirm), + onConfirm: () => dispatch(editStatus(status.get('id'), history)), })); } else { dispatch(editStatus(status.get('id'), history)); @@ -228,7 +231,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ onTranslate (status) { if (status.get('translation')) { - dispatch(undoStatusTranslation(status.get('id'), status.get('poll'))); + dispatch(undoStatusTranslation(status.get('id'))); } else { dispatch(translateStatus(status.get('id'))); } @@ -242,18 +245,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ dispatch(mentionCompose(account, router)); }, - onOpenMedia (statusId, media, index, lang) { - dispatch(openModal({ - modalType: 'MEDIA', - modalProps: { statusId, media, index, lang }, - })); + onOpenMedia (statusId, media, index) { + dispatch(openModal('MEDIA', { statusId, media, index })); }, - onOpenVideo (statusId, media, lang, options) { - dispatch(openModal({ - modalType: 'VIDEO', - modalProps: { statusId, media, lang, options }, - })); + onOpenVideo (statusId, media, options) { + dispatch(openModal('VIDEO', { statusId, media, options })); }, onBlock (status) { @@ -298,13 +295,10 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ }, onInteractionModal (type, status) { - dispatch(openModal({ - modalType: 'INTERACTION', - modalProps: { - type, - accountId: status.getIn(['account', 'id']), - url: status.get('url'), - }, + dispatch(openModal('INTERACTION', { + type, + accountId: status.getIn(['account', 'id']), + url: status.get('url'), })); }, diff --git a/app/javascript/flavours/glitch/extra_polyfills.js b/app/javascript/flavours/glitch/extra_polyfills.js new file mode 100644 index 000000000..e6c69de8b --- /dev/null +++ b/app/javascript/flavours/glitch/extra_polyfills.js @@ -0,0 +1,2 @@ +import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'; +import 'requestidlecallback'; diff --git a/app/javascript/flavours/glitch/features/about/index.jsx b/app/javascript/flavours/glitch/features/about/index.jsx index 42a3077de..1e0a8666a 100644 --- a/app/javascript/flavours/glitch/features/about/index.jsx +++ b/app/javascript/flavours/glitch/features/about/index.jsx @@ -1,21 +1,17 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; -import { Helmet } from 'react-helmet'; - +import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; - -import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server'; +import PropTypes from 'prop-types'; import Column from 'flavours/glitch/components/column'; -import { Icon } from 'flavours/glitch/components/icon'; -import { ServerHeroImage } from 'flavours/glitch/components/server_hero_image'; -import { Skeleton } from 'flavours/glitch/components/skeleton'; -import Account from 'flavours/glitch/containers/account_container'; import LinkFooter from 'flavours/glitch/features/ui/components/link_footer'; +import { Helmet } from 'react-helmet'; +import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server'; +import Account from 'flavours/glitch/containers/account_container'; +import Skeleton from 'flavours/glitch/components/skeleton'; +import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; +import Image from 'flavours/glitch/components/image'; const messages = defineMessages({ title: { id: 'column.about', defaultMessage: 'About' }, @@ -45,7 +41,7 @@ const mapStateToProps = state => ({ domainBlocks: state.getIn(['server', 'domainBlocks']), }); -class Section extends PureComponent { +class Section extends React.PureComponent { static propTypes = { title: PropTypes.string, @@ -71,7 +67,7 @@ class Section extends PureComponent { return (
    -
    +
    {title}
    @@ -84,7 +80,9 @@ class Section extends PureComponent { } -class About extends PureComponent { +export default @connect(mapStateToProps) +@injectIntl +class About extends React.PureComponent { static propTypes = { server: ImmutablePropTypes.map, @@ -118,7 +116,7 @@ class About extends PureComponent {
    - `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> + `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />

    {isLoading ? : server.get('domain')}

    Mastodon }} />

    @@ -220,5 +218,3 @@ class About extends PureComponent { } } - -export default connect(mapStateToProps)(injectIntl(About)); diff --git a/app/javascript/flavours/glitch/features/account/components/account_note.jsx b/app/javascript/flavours/glitch/features/account/components/account_note.jsx index 041f8de98..b5c0c9205 100644 --- a/app/javascript/flavours/glitch/features/account/components/account_note.jsx +++ b/app/javascript/flavours/glitch/features/account/components/account_note.jsx @@ -1,18 +1,16 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - +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 Icon from 'flavours/glitch/components/icon'; import Textarea from 'react-textarea-autosize'; -import { Icon } from 'flavours/glitch/components/icon'; - const messages = defineMessages({ placeholder: { id: 'account_note.glitch_placeholder', defaultMessage: 'No comment provided' }, }); +export default @injectIntl class Header extends ImmutablePureComponent { static propTypes = { @@ -56,11 +54,11 @@ class Header extends ImmutablePureComponent { if (isEditing) { action_buttons = (
    -
    -
    @@ -68,7 +66,7 @@ class Header extends ImmutablePureComponent { } else { action_buttons = (
    -
    @@ -104,5 +102,3 @@ class Header extends ImmutablePureComponent { } } - -export default injectIntl(Header); diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.jsx b/app/javascript/flavours/glitch/features/account/components/action_bar.jsx index 46a766925..d53080d4f 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.jsx @@ -1,17 +1,19 @@ -import { PureComponent } from 'react'; - -import { FormattedMessage, FormattedNumber } from 'react-intl'; - -import { NavLink } from 'react-router-dom'; - +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 { Icon } from 'flavours/glitch/components/icon'; - -class ActionBar extends PureComponent { +export default @injectIntl +class ActionBar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, }; isStatusesPageActive = (match, location) => { @@ -22,7 +24,7 @@ class ActionBar extends PureComponent { }; render () { - const { account } = this.props; + const { account, intl } = this.props; if (account.get('suspended')) { return ( @@ -81,5 +83,3 @@ class ActionBar extends PureComponent { } } - -export default ActionBar; diff --git a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx index 537d9854d..d646b08b2 100644 --- a/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx +++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.jsx @@ -1,10 +1,8 @@ +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({ @@ -12,6 +10,7 @@ const messages = defineMessages({ empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' }, }); +export default @injectIntl class FeaturedTags extends ImmutablePureComponent { static contextTypes = { @@ -36,7 +35,7 @@ class FeaturedTags extends ImmutablePureComponent {

    }} />

    - {featuredTags.map(featuredTag => ( + {featuredTags.take(3).map(featuredTag => ( { if (e.name !== 'AbortError') console.error(e); @@ -272,16 +271,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, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); } 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, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); } - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); } if (signedIn && isRemote) { @@ -290,7 +289,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, dangerous: true }); + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain }); } } @@ -314,7 +313,7 @@ class Header extends ImmutablePureComponent { let badge; if (account.get('bot')) { - badge = (
    ); + badge = (
    ); } else if (account.get('group')) { badge = (
    ); } else { @@ -348,10 +347,10 @@ class Header extends ImmutablePureComponent { {!suspended && (
    {!hidden && ( - <> + {actionBtn} {bellBtn} - + )} @@ -380,7 +379,7 @@ class Header extends ImmutablePureComponent {
    - {pair.get('verified_at') && } + {pair.get('verified_at') && }
    ))} @@ -389,7 +388,7 @@ class Header extends ImmutablePureComponent { {account.get('note').length > 0 && account.get('note') !== '

    ' &&
    } -
    +
    )} @@ -404,5 +403,3 @@ class Header extends ImmutablePureComponent { } } - -export default injectIntl(Header); diff --git a/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx b/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx index 2dc4216bd..17c08e375 100644 --- a/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx +++ b/app/javascript/flavours/glitch/features/account/components/profile_column_header.jsx @@ -1,15 +1,14 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { injectIntl, defineMessages } from 'react-intl'; - import ColumnHeader from '../../../components/column_header'; +import { injectIntl, defineMessages } from 'react-intl'; const messages = defineMessages({ profile: { id: 'column_header.profile', defaultMessage: 'Profile' }, }); -class ProfileColumnHeader extends PureComponent { +export default @injectIntl +class ProfileColumnHeader extends React.PureComponent { static propTypes = { onClick: PropTypes.func, @@ -32,5 +31,3 @@ class ProfileColumnHeader extends 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 51d229c84..f1d007ecb 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,7 +1,5 @@ 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 bafdcba80..6f0b06941 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,9 +1,7 @@ -import { List as ImmutableList } from 'immutable'; import { connect } from 'react-redux'; - -import { makeGetAccount } from 'flavours/glitch/selectors'; - import FeaturedTags from '../components/featured_tags'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import { List as ImmutableList } from 'immutable'; 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 3b2ffbadf..c6a3afb7e 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,8 +1,6 @@ import { connect } from 'react-redux'; - -import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts'; - import FollowRequestNote from '../components/follow_request_note'; +import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts'; const mapDispatchToProps = (dispatch, { account }) => ({ onAuthorize () { diff --git a/app/javascript/flavours/glitch/features/account/navigation.jsx b/app/javascript/flavours/glitch/features/account/navigation.jsx index 4be00c49f..edae38ce5 100644 --- a/app/javascript/flavours/glitch/features/account/navigation.jsx +++ b/app/javascript/flavours/glitch/features/account/navigation.jsx @@ -1,8 +1,6 @@ +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'; @@ -21,7 +19,8 @@ const mapStateToProps = (state, { match: { params: { acct } } }) => { }; }; -class AccountNavigation extends PureComponent { +export default @connect(mapStateToProps) +class AccountNavigation extends React.PureComponent { static propTypes = { match: PropTypes.shape({ @@ -51,5 +50,3 @@ class AccountNavigation extends PureComponent { } } - -export default connect(mapStateToProps)(AccountNavigation); diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx index 4453b557d..5fd84996b 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.jsx @@ -1,16 +1,12 @@ -import PropTypes from 'prop-types'; - +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 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 = { diff --git a/app/javascript/flavours/glitch/features/account_gallery/index.jsx b/app/javascript/flavours/glitch/features/account_gallery/index.jsx index 3a9f07d76..afd6e5161 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/index.jsx +++ b/app/javascript/flavours/glitch/features/account_gallery/index.jsx @@ -1,25 +1,21 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +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 { 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 LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import Column from 'flavours/glitch/features/ui/components/column'; -import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; +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 { openModal } from 'flavours/glitch/actions/modal'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; const mapStateToProps = (state, { params: { acct, id } }) => { const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]); @@ -62,6 +58,7 @@ class LoadMoreMedia extends ImmutablePureComponent { } +export default @connect(mapStateToProps) class AccountGallery extends ImmutablePureComponent { static propTypes = { @@ -75,8 +72,8 @@ class AccountGallery extends ImmutablePureComponent { isLoading: PropTypes.bool, hasMore: PropTypes.bool, isAccount: PropTypes.bool, - suspended: PropTypes.bool, multiColumn: PropTypes.bool, + suspended: PropTypes.bool, }; state = { @@ -145,26 +142,16 @@ class AccountGallery extends ImmutablePureComponent { 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({ - modalType: 'VIDEO', - modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } }, - })); + dispatch(openModal('VIDEO', { media: attachment, statusId, options: { autoPlay: true } })); } else if (attachment.get('type') === 'audio') { - dispatch(openModal({ - modalType: 'AUDIO', - modalProps: { media: attachment, statusId, lang, options: { autoPlay: true } }, - })); + dispatch(openModal('AUDIO', { media: attachment, statusId, options: { autoPlay: true } })); } else { const media = attachment.getIn(['status', 'media_attachments']); const index = media.findIndex(x => x.get('id') === attachment.get('id')); - dispatch(openModal({ - modalType: 'MEDIA', - modalProps: { media, index, statusId, lang }, - })); + dispatch(openModal('MEDIA', { media, index, statusId })); } }; @@ -180,7 +167,9 @@ class AccountGallery extends ImmutablePureComponent { if (!isAccount) { return ( - + + + ); } @@ -199,7 +188,7 @@ class AccountGallery extends ImmutablePureComponent { } return ( - + @@ -234,5 +223,3 @@ class AccountGallery extends ImmutablePureComponent { } } - -export default connect(mapStateToProps)(AccountGallery); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx index 717114d5c..eec065b43 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.jsx @@ -1,16 +1,11 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import { NavLink } from 'react-router-dom'; - +import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import ActionBar from 'flavours/glitch/features/account/components/action_bar'; +import PropTypes from 'prop-types'; import InnerHeader from 'flavours/glitch/features/account/components/header'; - -import MemorialNote from './memorial_note'; +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 MovedNote from './moved_note'; export default class Header extends ImmutablePureComponent { @@ -121,7 +116,6 @@ 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.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx index 5ea37a5d3..dc2b3e3e6 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/limited_account_hint.jsx @@ -1,11 +1,8 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - import { connect } from 'react-redux'; - import { revealAccount } from 'flavours/glitch/actions/accounts'; +import { FormattedMessage } from 'react-intl'; import Button from 'flavours/glitch/components/button'; import { domain } from 'flavours/glitch/initial_state'; @@ -17,7 +14,8 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({ }); -class LimitedAccountHint extends PureComponent { +export default @connect(() => {}, mapDispatchToProps) +class LimitedAccountHint extends React.PureComponent { static propTypes = { accountId: PropTypes.string.isRequired, @@ -36,5 +34,3 @@ class LimitedAccountHint extends PureComponent { } } - -export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx index 2e10ea94a..40bdc4034 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx @@ -1,14 +1,11 @@ +import React from 'react'; import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; 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 DisplayName from '../../../components/display_name'; +import Icon from 'flavours/glitch/components/icon'; export default class MovedNote extends ImmutablePureComponent { @@ -24,7 +21,9 @@ export default class MovedNote extends ImmutablePureComponent { handleAccountClick = e => { if (e.button === 0) { e.preventDefault(); - this.context.router.history.push(`/@${this.props.to.get('acct')}`); + 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); } e.stopPropagation(); @@ -38,7 +37,7 @@ export default class MovedNote extends ImmutablePureComponent {
    - }} /> + }} />
    diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx index 270865df4..3ec47cf2f 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx @@ -1,8 +1,7 @@ -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - +import React from 'react'; import { connect } from 'react-redux'; - -import { initEditAccountNote } from 'flavours/glitch/actions/account_notes'; +import { makeGetAccount, getAccountHidden } from 'flavours/glitch/selectors'; +import Header from '../components/header'; import { followAccount, unfollowAccount, @@ -11,24 +10,23 @@ import { pinAccount, unpinAccount, } from 'flavours/glitch/actions/accounts'; -import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { mentionCompose, directCompose, } from 'flavours/glitch/actions/compose'; -import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks'; -import { openModal } from 'flavours/glitch/actions/modal'; 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 { 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: 'Block entire domain' }, + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, }); const makeMapStateToProps = () => { @@ -48,26 +46,20 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { if (account.getIn(['relationship', 'following'])) { if (unfollowModal) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - }, + dispatch(openModal('CONFIRM', { + 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({ - modalType: 'CONFIRM', - modalProps: { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - }, + dispatch(openModal('CONFIRM', { + message: @{account.get('acct')} }} />, + confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), })); } else { dispatch(unfollowAccount(account.get('id'))); @@ -78,13 +70,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onInteractionModal (account) { - dispatch(openModal({ - modalType: 'INTERACTION', - modalProps: { - type: 'follow', - accountId: account.get('id'), - url: account.get('url'), - }, + dispatch(openModal('INTERACTION', { + type: 'follow', + accountId: account.get('id'), + url: account.get('url'), })); }, @@ -104,6 +93,10 @@ 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 })); @@ -145,13 +138,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onBlockDomain (domain) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: {domain} }} />, - confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain)), - }, + dispatch(openModal('CONFIRM', { + message: {domain} }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), })); }, @@ -160,30 +150,21 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, onAddToList (account) { - dispatch(openModal({ - modalType: 'LIST_ADDER', - modalProps: { - accountId: account.get('id'), - }, + dispatch(openModal('LIST_ADDER', { + accountId: account.get('id'), })); }, onChangeLanguages (account) { - dispatch(openModal({ - modalType: 'SUBSCRIBED_LANGUAGES', - modalProps: { - accountId: account.get('id'), - }, + dispatch(openModal('SUBSCRIBED_LANGUAGES', { + accountId: account.get('id'), })); }, onOpenAvatar (account) { - dispatch(openModal({ - modalType: 'IMAGE', - modalProps: { - src: account.get('avatar'), - alt: account.get('acct'), - }, + dispatch(openModal('IMAGE', { + src: account.get('avatar'), + alt: account.get('acct'), })); }, diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.jsx b/app/javascript/flavours/glitch/features/account_timeline/index.jsx index 55631c7b5..9151c1990 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/account_timeline/index.jsx @@ -1,34 +1,24 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import { List as ImmutableList } from 'immutable'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +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 { 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 { 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 LimitedAccountHint from './components/limited_account_hint'; +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 { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; +import { fetchFeaturedTags } from '../../actions/featured_tags'; const emptyList = ImmutableList(); @@ -72,6 +62,7 @@ RemoteHint.propTypes = { url: PropTypes.string.isRequired, }; +export default @connect(mapStateToProps) class AccountTimeline extends ImmutablePureComponent { static propTypes = { @@ -133,7 +124,7 @@ class AccountTimeline extends ImmutablePureComponent { } } - UNSAFE_componentWillReceiveProps (nextProps) { + componentWillReceiveProps (nextProps) { const { dispatch } = this.props; if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { @@ -170,7 +161,10 @@ class AccountTimeline extends ImmutablePureComponent { ); } else if (!isLoading && !isAccount) { return ( - + + + + ); } @@ -191,7 +185,7 @@ class AccountTimeline extends ImmutablePureComponent { const remoteMessage = remote ? : null; return ( - + +
    }
    @@ -547,7 +533,7 @@ class Audio extends PureComponent { @@ -564,7 +550,7 @@ class Audio extends PureComponent {
    @@ -589,5 +575,3 @@ class Audio extends PureComponent { } } - -export default injectIntl(Audio); diff --git a/app/javascript/flavours/glitch/features/blocks/index.jsx b/app/javascript/flavours/glitch/features/blocks/index.jsx index aa5479b20..4461bd14d 100644 --- a/app/javascript/flavours/glitch/features/blocks/index.jsx +++ b/app/javascript/flavours/glitch/features/blocks/index.jsx @@ -1,20 +1,16 @@ -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 React from 'react'; import { connect } from 'react-redux'; - +import ImmutablePropTypes from 'react-immutable-proptypes'; 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 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 ImmutablePureComponent from 'react-immutable-pure-component'; const messages = defineMessages({ heading: { id: 'column.blocks', defaultMessage: 'Blocked users' }, @@ -26,6 +22,8 @@ const mapStateToProps = state => ({ isLoading: state.getIn(['user_lists', 'blocks', 'isLoading'], true), }); +export default @connect(mapStateToProps) +@injectIntl class Blocks extends ImmutablePureComponent { static propTypes = { @@ -38,7 +36,7 @@ class Blocks extends ImmutablePureComponent { multiColumn: PropTypes.bool, }; - UNSAFE_componentWillMount () { + componentWillMount () { this.props.dispatch(fetchBlocks()); } @@ -79,5 +77,3 @@ class Blocks extends ImmutablePureComponent { } } - -export default connect(mapStateToProps)(injectIntl(Blocks)); diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx index fe8b883de..8e25bc6fd 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx @@ -1,15 +1,11 @@ +import { debounce } from 'lodash'; import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - +import React from 'react'; import { Helmet } from 'react-helmet'; - +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 { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'flavours/glitch/actions/bookmarks'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import ColumnHeader from 'flavours/glitch/components/column_header'; @@ -26,6 +22,8 @@ const mapStateToProps = state => ({ hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']), }); +export default @connect(mapStateToProps) +@injectIntl class Bookmarks extends ImmutablePureComponent { static propTypes = { @@ -38,7 +36,7 @@ class Bookmarks extends ImmutablePureComponent { isLoading: PropTypes.bool, }; - UNSAFE_componentWillMount () { + componentWillMount () { this.props.dispatch(fetchBookmarkedStatuses()); } @@ -108,5 +106,3 @@ class Bookmarks extends ImmutablePureComponent { } } - -export default connect(mapStateToProps)(injectIntl(Bookmarks)); diff --git a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx index b556da391..bdaa9885c 100644 --- a/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx +++ b/app/javascript/flavours/glitch/features/closed_registrations_modal/index.jsx @@ -1,15 +1,15 @@ -import { FormattedMessage } from 'react-intl'; - -import ImmutablePureComponent from 'react-immutable-pure-component'; +import React from 'react'; import { connect } from 'react-redux'; - -import { fetchServer } from 'flavours/glitch/actions/server'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; import { domain } from 'flavours/glitch/initial_state'; +import { fetchServer } from 'flavours/glitch/actions/server'; const mapStateToProps = state => ({ message: state.getIn(['server', 'server', 'registrations', 'message']), }); +export default @connect(mapStateToProps) class ClosedRegistrationsModal extends ImmutablePureComponent { componentDidMount () { @@ -73,5 +73,3 @@ class ClosedRegistrationsModal extends ImmutablePureComponent { } } - -export default connect(mapStateToProps)(ClosedRegistrationsModal); diff --git a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx index 1e93125d5..69a4699ac 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/community_timeline/components/column_settings.jsx @@ -1,10 +1,7 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - import ImmutablePropTypes from 'react-immutable-proptypes'; - +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import SettingText from 'flavours/glitch/components/setting_text'; import SettingToggle from 'flavours/glitch/features/notifications/components/setting_toggle'; @@ -13,7 +10,8 @@ const messages = defineMessages({ settings: { id: 'home.settings', defaultMessage: 'Column settings' }, }); -class ColumnSettings extends PureComponent { +export default @injectIntl +class ColumnSettings extends React.PureComponent { static propTypes = { settings: ImmutablePropTypes.map.isRequired, @@ -41,5 +39,3 @@ class ColumnSettings extends 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 dbfc4594e..eac1c4bba 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,10 +1,8 @@ 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']); diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.jsx b/app/javascript/flavours/glitch/features/community_timeline/index.jsx index 127e7cf18..b9a59fdc7 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/community_timeline/index.jsx @@ -1,22 +1,17 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import { Helmet } from 'react-helmet'; - +import React from 'react'; 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 { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; import Column from 'flavours/glitch/components/column'; import ColumnHeader from 'flavours/glitch/components/column_header'; -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 { 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'; const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, @@ -37,7 +32,9 @@ const mapStateToProps = (state, { columnId }) => { }; }; -class CommunityTimeline extends PureComponent { +export default @connect(mapStateToProps) +@injectIntl +class CommunityTimeline extends React.PureComponent { static defaultProps = { onlyMedia: false, @@ -165,5 +162,3 @@ class CommunityTimeline extends PureComponent { } } - -export default connect(mapStateToProps)(injectIntl(CommunityTimeline)); diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx index 2a6202f84..1843fdacb 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -1,13 +1,9 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { defineMessages, injectIntl } from 'react-intl'; - +import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; - -import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links'; - +import PropTypes from 'prop-types'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; +import { defineMessages, injectIntl } from 'react-intl'; +import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links'; const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -18,14 +14,15 @@ 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: 'Blocked domains' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden 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' }, }); -class ActionBar extends PureComponent { +export default @injectIntl +class ActionBar extends React.PureComponent { static propTypes = { account: ImmutablePropTypes.map.isRequired, @@ -69,5 +66,3 @@ class ActionBar extends PureComponent { } } - -export default injectIntl(ActionBar); diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx index 5f00da52c..fb9bb5035 100644 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +++ 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.jsx b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx index 42452b30f..0ecfc9141 100644 --- a/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx @@ -1,9 +1,8 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - import { length } from 'stringz'; -export default class CharacterCounter extends PureComponent { +export default class CharacterCounter extends React.PureComponent { static propTypes = { text: PropTypes.string.isRequired, diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index 53e1bf79a..3ed10697d 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -1,28 +1,24 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl } from 'react-intl'; - +import React from 'react'; 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 PropTypes from 'prop-types'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; +import QuoteIndicatorContainer from '../containers/quote_indicator_container'; +import AutosuggestTextarea from '../../../components/autosuggest_textarea'; +import AutosuggestInput from '../../../components/autosuggest_input'; +import { defineMessages, injectIntl } from 'react-intl'; +import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; +import PollFormContainer from '../containers/poll_form_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 CharacterCounter from './character_counter'; +import OptionsContainer from '../containers/options_container'; 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?' }, @@ -37,6 +33,7 @@ const messages = defineMessages({ spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, }); +export default @injectIntl class ComposeForm extends ImmutablePureComponent { static contextTypes = { @@ -81,6 +78,7 @@ class ComposeForm extends ImmutablePureComponent { preselectOnReply: PropTypes.bool, onChangeSpoilerness: PropTypes.func, onChangeVisibility: PropTypes.func, + onPaste: PropTypes.func, onMediaDescriptionConfirm: PropTypes.func, }; @@ -168,11 +166,11 @@ class ComposeForm extends ImmutablePureComponent { }; // Selects a suggestion from the autofill. - handleSuggestionSelected = (tokenStart, token, value) => { + onSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['text']); }; - handleSpoilerSuggestionSelected = (tokenStart, token, value) => { + onSpoilerSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); }; @@ -181,7 +179,7 @@ class ComposeForm extends ImmutablePureComponent { this.handleSubmit(); } - if (e.keyCode === 13 && e.altKey) { + if (e.keyCode == 13 && e.altKey) { this.handleSecondarySubmit(); } }; @@ -285,7 +283,9 @@ class ComposeForm extends ImmutablePureComponent { const { handleEmojiPick, handleSecondarySubmit, + handleSelect, handleSubmit, + handleRefTextarea, } = this; const { advancedOptions, @@ -293,6 +293,7 @@ class ComposeForm extends ImmutablePureComponent { isSubmitting, layout, onChangeSpoilerness, + onChangeVisibility, onClearSuggestions, onFetchSuggestions, onPaste, @@ -314,6 +315,7 @@ class ComposeForm extends ImmutablePureComponent { +
    { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); - e.stopPropagation(); } }; @@ -54,8 +53,8 @@ export default class ComposerOptionsDropdownContent extends PureComponent { // On mounting, we add our listeners. componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, withPassive); if (this.focusedItem) { this.focusedItem.focus({ preventScroll: true }); } else { @@ -65,8 +64,8 @@ export default class ComposerOptionsDropdownContent extends PureComponent { // On unmounting, we remove our listeners. componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, withPassive); } handleClick = (e) => { @@ -76,10 +75,10 @@ export default class ComposerOptionsDropdownContent extends PureComponent { onChange, onClose, closeOnChange, + items, } = this.props; const { name } = this.props.items[i]; - e.preventDefault(); // Prevents change in focus on click if (closeOnChange) { onClose(); @@ -98,6 +97,7 @@ export default class ComposerOptionsDropdownContent extends PureComponent { handleKeyDown = (e) => { const index = Number(e.currentTarget.getAttribute('data-index')); + const { items } = this.props; let element = null; switch(e.key) { @@ -152,14 +152,14 @@ export default class ComposerOptionsDropdownContent extends PureComponent { if (!contents) { contents = ( - <> + {icon && }
    {text} {meta}
    - +
    ); } @@ -169,8 +169,7 @@ export default class ComposerOptionsDropdownContent extends PureComponent { onClick={this.handleClick} onKeyDown={this.handleKeyDown} role='option' - aria-selected={active} - tabIndex={0} + tabIndex='0' key={name} data-index={i} ref={active ? this.setFocusRef : null} @@ -184,6 +183,8 @@ export default class ComposerOptionsDropdownContent extends PureComponent { render () { const { items, + onChange, + onClose, style, } = this.props; diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index 20e499805..0f7c021b6 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -1,21 +1,15 @@ +import React from 'react'; import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; - -import { supportsPassiveEvents } from 'detect-passive-events'; +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 { 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...' }, @@ -34,7 +28,7 @@ const messages = defineMessages({ let EmojiPicker, Emoji; // load asynchronously -const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`; @@ -54,7 +48,7 @@ const notFoundFn = () => (
    ); -class ModifierPickerMenu extends PureComponent { +class ModifierPickerMenu extends React.PureComponent { static propTypes = { active: PropTypes.bool, @@ -66,7 +60,7 @@ class ModifierPickerMenu extends PureComponent { this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); }; - UNSAFE_componentWillReceiveProps (nextProps) { + componentWillReceiveProps (nextProps) { if (nextProps.active) { this.attachListeners(); } else { @@ -85,12 +79,12 @@ class ModifierPickerMenu extends PureComponent { }; attachListeners () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); } removeListeners () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('click', this.handleDocumentClick, false); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -115,7 +109,7 @@ class ModifierPickerMenu extends PureComponent { } -class ModifierPicker extends PureComponent { +class ModifierPicker extends React.PureComponent { static propTypes = { active: PropTypes.bool, @@ -151,7 +145,8 @@ class ModifierPicker extends PureComponent { } -class EmojiPickerMenuImpl extends PureComponent { +@injectIntl +class EmojiPickerMenu extends React.PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, @@ -183,7 +178,7 @@ class EmojiPickerMenuImpl extends PureComponent { }; componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); + document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need @@ -198,7 +193,7 @@ class EmojiPickerMenuImpl extends PureComponent { } componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, { capture: true }); + document.removeEventListener('click', this.handleDocumentClick, false); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -312,9 +307,8 @@ class EmojiPickerMenuImpl extends PureComponent { } -const EmojiPickerMenu = injectIntl(EmojiPickerMenuImpl); - -class EmojiPickerDropdown extends PureComponent { +export default @injectIntl +class EmojiPickerDropdown extends React.PureComponent { static propTypes = { custom_emojis: ImmutablePropTypes.list, @@ -418,5 +412,3 @@ class EmojiPickerDropdown extends PureComponent { } } - -export default injectIntl(EmojiPickerDropdown); diff --git a/app/javascript/flavours/glitch/features/compose/components/header.jsx b/app/javascript/flavours/glitch/features/compose/components/header.jsx index ac6d4dce8..dcbdafa57 100644 --- a/app/javascript/flavours/glitch/features/compose/components/header.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/header.jsx @@ -1,16 +1,19 @@ +// Package imports. import PropTypes from 'prop-types'; - -import { injectIntl, defineMessages } from 'react-intl'; - -import { Link } from 'react-router-dom'; - +import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, defineMessages } from 'react-intl'; +import { Link } from 'react-router-dom'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { Icon } from 'flavours/glitch/components/icon'; -import { signOutLink } from 'flavours/glitch/utils/backend_links'; -import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; +// Components. +import Icon from 'flavours/glitch/components/icon'; +// Utils. +import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; +import { signOutLink } from 'flavours/glitch/utils/backend_links'; + +// Messages. const messages = defineMessages({ community: { defaultMessage: 'Local timeline', @@ -42,6 +45,7 @@ const messages = defineMessages({ }, }); +export default @injectIntl class Header extends ImmutablePureComponent { static propTypes = { @@ -74,7 +78,7 @@ class Header extends ImmutablePureComponent { // The result. return ( -